From 2fdad79dbb588a5ba80afb21157d5aaa8a4a2927 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 8 Dec 2024 19:23:54 -0800 Subject: [PATCH] SMTP module work --- README.md | 2 +- etc/db_admin.py | 25 +++++++++++ mesh_bot.py | 7 +++ modules/settings.py | 71 +++++++++++++++++++----------- modules/smtp.py | 103 +++++++++++++++++++++----------------------- modules/system.py | 6 +++ 6 files changed, 134 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 523c1a7..9ec8f83 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,7 @@ In the config.ini enable the module # enable or disable the scheduler module enabled = True ``` - The actions are via code only at this time. See mesh_bot.py around line [1050](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1050) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost. + The actions are via code only at this time. See mesh_bot.py around line [1097](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1097) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost. ```python #Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1 diff --git a/etc/db_admin.py b/etc/db_admin.py index ae04f27..eb7d855 100644 --- a/etc/db_admin.py +++ b/etc/db_admin.py @@ -23,6 +23,27 @@ except Exception as e: except Exception as e: bbs_dm = "System: data/bbsdm.pkl not found" +try: + with open('../data/email_db.pickle', 'rb') as f: + email_db = pickle.load(f) +except Exception as e: + try: + with open('data/email_db.pickle', 'rb') as f: + email_db = pickle.load(f) + except Exception as e: + email_db = "System: data/email_db.pickle not found" + +try: + with open('../data/sms_db.pickle', 'rb') as f: + sms_db = pickle.load(f) +except Exception as e: + try: + with open('data/sms_db.pickle', 'rb') as f: + sms_db = pickle.load(f) + except Exception as e: + sms_db = "System: data/sms_db.pickle not found" + + # Game HS tables try: with open('../data/lemonstand.pkl', 'rb') as f: @@ -90,6 +111,10 @@ print ("System: bbs_messages") print (bbs_messages) print ("\nSystem: bbs_dm") print (bbs_dm) +print ("\nSystem: email_db") +print (email_db) +print ("\nSystem: sms_db") +print (sms_db) print (f"\n\nGame HS tables\n") print (f"lemon:{lemon_score}") print (f"dopewar:{dopewar_score}") diff --git a/mesh_bot.py b/mesh_bot.py index e1b29b3..54f821f 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -37,11 +37,13 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n "bbspost": lambda: handle_bbspost(message, message_from_id, deviceID), "bbsread": lambda: handle_bbsread(message), "blackjack": lambda: handleBlackJack(message, message_from_id, deviceID), + "clearsms:": lambda: handle_sms(message_from_id, message), "cmd": lambda: help_message, "cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "dopewars": lambda: handleDopeWars(message, message_from_id, deviceID), + "email:": lambda: handle_email(message_from_id, message), "games": lambda: gamesCmdList, "globalthermonuclearwar": lambda: handle_gTnW(), "golfsim": lambda: handleGolf(message, message_from_id, deviceID), @@ -59,7 +61,10 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n "pong": lambda: "🏓PING!!🛜", "readnews": lambda: read_news(), "rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number), + "setemail:": lambda: handle_email(message_from_id, message), + "setsms:": lambda: handle_sms( message_from_id, message), "sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM), + "sms:": lambda: handle_sms(message_from_id, message), "solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(), "sun": lambda: handle_sun(message_from_id, deviceID, channel_number), "test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), @@ -1092,6 +1097,8 @@ async def start_rx(): logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}") if emergency_responder_enabled: logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}") + if enableSMTP: + logger.debug(f"System: SMTP Email Alerting Enabled") if scheduler_enabled: # Examples of using the scheduler, Times here are in 24hr format # https://schedule.readthedocs.io/en/stable/ diff --git a/modules/settings.py b/modules/settings.py index da4e5c4..df35150 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -41,50 +41,53 @@ except Exception as e: if config.sections() == []: print(f"System: Error reading config file: {config_file} is empty or does not exist.") config['interface'] = {'type': 'serial', 'port': "/dev/ttyACM0", 'hostname': '', 'mac': ''} - config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD, - 'welcome_message': WELCOME_MSG, 'zuluTime': 'False'} + config['general'] = {'respond_by_dm_only': 'True', 'defaultChannel': '0', 'motd': MOTD, 'welcome_message': WELCOME_MSG, 'zuluTime': 'False'} config.write(open(config_file, 'w')) print (f"System: Config file created, check {config_file} or review the config.template") if 'sentry' not in config: - config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'} - config.write(open(config_file, 'w')) + config['sentry'] = {'SentryEnabled': 'False', 'SentryChannel': '2', 'SentryHoldoff': '9', 'sentryIgnoreList': '', 'SentryRadius': '100'} + config.write(open(config_file, 'w')) if 'location' not in config: - config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'} - config.write(open(config_file, 'w')) + config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'} + config.write(open(config_file, 'w')) if 'bbs' not in config: - config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''} - config.write(open(config_file, 'w')) + config['bbs'] = {'enabled': 'False', 'bbsdb': 'data/bbsdb.pkl', 'bbs_ban_list': '', 'bbs_admin_list': ''} + config.write(open(config_file, 'w')) if 'repeater' not in config: - config['repeater'] = {'enabled': 'False', 'repeater_channels': ''} - config.write(open(config_file, 'w')) + config['repeater'] = {'enabled': 'False', 'repeater_channels': ''} + config.write(open(config_file, 'w')) if 'radioMon' not in config: - config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'} - config.write(open(config_file, 'w')) + config['radioMon'] = {'enabled': 'False', 'rigControlServerAddress': 'localhost:4532', 'sigWatchBrodcastCh': '2', 'signalDetectionThreshold': '-10', 'signalHoldTime': '10', 'signalCooldown': '5', 'signalCycleLimit': '5'} + config.write(open(config_file, 'w')) if 'games' not in config: - config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'} - config.write(open(config_file, 'w')) + config['games'] = {'dopeWars': 'True', 'lemonade': 'True', 'blackjack': 'True', 'videoPoker': 'True'} + config.write(open(config_file, 'w')) if 'messagingSettings' not in config: - config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'} - config.write(open(config_file, 'w')) + config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'} + config.write(open(config_file, 'w')) if 'fileMon' not in config: - config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'} - config.write(open(config_file, 'w')) + config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'} + config.write(open(config_file, 'w')) if 'scheduler' not in config: - config['scheduler'] = {'enabled': 'False'} - config.write(open(config_file, 'w')) + config['scheduler'] = {'enabled': 'False'} + config.write(open(config_file, 'w')) if 'emergencyHandler' not in config: - config['emergencyHandler'] = {'enabled': 'False', 'alert_channel': '2', 'alert_interface': '1', 'email': ''} - config.write(open(config_file, 'w')) + config['emergencyHandler'] = {'enabled': 'False', 'alert_channel': '2', 'alert_interface': '1', 'email': ''} + config.write(open(config_file, 'w')) + +if 'smtp' not in config: + config['smtp'] = {'sysopEmails': '', 'enableSMTP': 'False', 'enableImap': 'False'} + config.write(open(config_file, 'w')) # interface1 settings interface1_type = config['interface'].get('type', 'serial') @@ -102,7 +105,7 @@ if 'interface2' in config: else: interface2_enabled = False -# variables +# variables from the config.ini file try: # general useDMForResponse = config['general'].getboolean('respond_by_dm_only', True) @@ -161,7 +164,7 @@ try: wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',') else: wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2 - + # bbs bbs_enabled = config['bbs'].getboolean('enabled', False) bbsdb = config['bbs'].get('bbsdb', 'data/bbsdb.pkl') @@ -170,6 +173,26 @@ try: bbs_link_enabled = config['bbs'].getboolean('bbslink_enabled', False) bbs_link_whitelist = config['bbs'].get('bbslink_whitelist', '').split(',') + # E-Mail Settings + sysopEmails = config['smtp'].get('sysopEmails', '').split(',') + enableSMTP = config['smtp'].getboolean('enableSMTP', False) + enableImap = config['smtp'].getboolean('enableImap', False) + + # SMTP settings (required for outbound email/sms) + SMTP_SERVER = "smtp.gmail.com" # Replace with your SMTP server + SMTP_PORT = 587 # 587 SMTP over TLS/STARTTLS, 25 legacy SMTP + FROM_EMAIL = "your_email@gmail.com" # Sender email: be mindful of public access, don't use your personal email + SMTP_USERNAME = "your_email@gmail.com" # Sender email username + SMTP_PASSWORD = "your_app_password" # Sender email password + EMAIL_SUBJECT = "Meshtastic✉️" + + # IMAP settings (inbound email) + IMAP_SERVER = "imap.gmail.com" # Replace with your IMAP server + IMAP_PORT = 993 # 993 IMAP over TLS/SSL, 143 legacy IMAP + IMAP_USERNAME = SMTP_USERNAME # IMAP username usually same as SMTP + IMAP_PASSWORD = SMTP_PASSWORD # IMAP password usually same as SMTP + IMAP_FOLDER = "inbox" # IMAP folder to monitor for new messages + # repeater repeater_enabled = config['repeater'].getboolean('enabled', False) repeater_channels = config['repeater'].get('repeater_channels', '').split(',') diff --git a/modules/smtp.py b/modules/smtp.py index d4e3fb6..1a4855f 100644 --- a/modules/smtp.py +++ b/modules/smtp.py @@ -10,26 +10,7 @@ import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart -# System settings -sysopEmails = ["spud@demo.net", ] # list of authorized emails for sysop control - -# SMTP settings (required for outbound email/sms) -SMTP_SERVER = "smtp.gmail.com" # Replace with your SMTP server -SMTP_PORT = 587 # 587 SMTP over TLS/STARTTLS, 25 legacy SMTP -FROM_EMAIL = "your_email@gmail.com" # Sender email: be mindful of public access, don't use your personal email -SMTP_USERNAME = "your_email@gmail.com" # Sender email username -SMTP_PASSWORD = "your_app_password" # Sender email password -EMAIL_SUBJECT = "Meshtastic✉️" - -# IMAP settings (inbound email) -enableImap = False -IMAP_SERVER = "imap.gmail.com" # Replace with your IMAP server -IMAP_PORT = 993 # 993 IMAP over TLS/SSL, 143 legacy IMAP -IMAP_USERNAME = SMTP_USERNAME # IMAP username usually same as SMTP -IMAP_PASSWORD = SMTP_PASSWORD # IMAP password usually same as SMTP -IMAP_FOLDER = "inbox" # IMAP folder to monitor for new messages - -# System variables // Do not edit +# System variables trap_list_smtp = ("email:", "setemail:", "sms:", "setsms:", "clearsms:") smtpThrottle = {} @@ -38,7 +19,6 @@ if enableImap: import imaplib import email - # Send email def send_email(to_email, message, nodeID=0): global smtpThrottle @@ -54,7 +34,7 @@ def send_email(to_email, message, nodeID=0): if nodeID in bbs_ban_list: logger.warning("System: Email blocked for " + nodeID) return "⛔️Email throttled, try again later" - + try: # Create message msg = MIMEMultipart() @@ -130,21 +110,20 @@ except: def store_email(nodeID, email): global email_db + # if not in db, add it logger.debug("System: Setting E-Mail for " + nodeID) - if nodeID not in email_db: - email_db[nodeID] = email - return True - # if in db, update it email_db[nodeID] = email # save to a pickle for persistence, this is a simple db, be mindful of risk with open('data/email_db.pickle', 'wb') as f: pickle.dump(email_db, f) + f.close() return True + # initalize SMS db -sms_db = {} +sms_db = [{'nodeID': 0, 'sms':[]}] try: with open('data/sms_db.pickle', 'rb') as f: sms_db = pickle.load(f) @@ -155,32 +134,45 @@ except: def store_sms(nodeID, sms): global sms_db - # if not in db, add it - logger.debug("System: Setting SMS for " + nodeID) - if nodeID not in sms_db: - sms_db[nodeID] = sms - return True - # if in db, append it - sms_db[nodeID].append(sms) + try: + logger.debug("System: Setting SMS for " + str(nodeID)) + # if not in db, add it + if nodeID not in sms_db: + sms_db.append({'nodeID': nodeID, 'sms': sms}) + else: + # if in db, update it + for item in sms_db: + if item['nodeID'] == nodeID: + item['sms'].append(sms) - # save to a pickle for persistence, this is a simple db, be mindful of risk - with open('data/sms_db.pickle', 'wb') as f: - pickle.dump(sms_db, f) - return True + # save to a pickle for persistence, this is a simple db, be mindful of risk + with open('data/sms_db.pickle', 'wb') as f: + pickle.dump(sms_db, f) + f.close() + return True + except Exception as e: + logger.warning("System: Failed to store SMS: " + str(e)) + return False def handle_sms(nodeID, message): + global sms_db # if clearsms, remove all sms for node - if message.lower.startswith("clearsms:"): - if nodeID in sms_db: - del sms_db[nodeID] + if message.lower().startswith("clearsms:"): + if any(item['nodeID'] == nodeID for item in sms_db): + # remove record from db for nodeID + sms_db = [item for item in sms_db if item['nodeID'] != nodeID] + # update the pickle + with open('data/sms_db.pickle', 'wb') as f: + pickle.dump(sms_db, f) + f.close() return "📲 address cleared" return "📲No address to clear" # send SMS to SMS in db. if none ask for one - if message.lower.startswith("setsms:"): + if message.lower().startswith("setsms:"): message = message.split(" ", 1) - if len(message) < 5: - return "?📲setsms example@phone.co" + if len(message[1]) < 5: + return "?📲setsms: example@phone.co" if "@" not in message[1] and "." not in message[1]: return "📲Please provide a valid email address" if store_sms(nodeID, message[1]): @@ -188,14 +180,17 @@ def handle_sms(nodeID, message): else: return "⛔️Failed to set address" - if message.lower.startswith("sms:"): + if message.lower().startswith("sms:"): message = message.split(" ", 1) - if nodeID in sms_db: + if any(item['nodeID'] == nodeID for item in sms_db): count = 0 - for address in sms_db[nodeID]: - count += 1 - logger.info("System: Sending SMS for " + nodeID) - send_email(address, message[1], nodeID) + # for all dict items maching nodeID in sms_db send sms + for item in sms_db: + if item['nodeID'] == nodeID: + smsEmail = item['sms'] + logger.info("System: Sending SMS for " + str(nodeID) + " to " + smsEmail[:-6]) + send_email(smsEmail, message[1], nodeID) + count += 1 return f"📲SMS-sent {count} 📤" else: return "📲No address set, use 📲setsms" @@ -203,11 +198,10 @@ def handle_sms(nodeID, message): return "Error: ⛔️ not understood. use:setsms example@phone.co" def handle_email(nodeID, message): + global email_db # send email to email in db. if none ask for one - if message.lower.startswith("setemail:"): + if message.lower().startswith("setemail:"): message = message.split(" ", 1) - if len(message) < 5: - return "?📧setemail example@none.net" if "@" not in message[1] and "." not in message[1]: return "📧Please provide a valid email address" if store_email(nodeID, message[1]): @@ -215,7 +209,7 @@ def handle_email(nodeID, message): return "Error: ⛔️ not understood. use:setmail bob@example.com" - if message.lower.startswith("email:"): + if message.lower().startswith("email:"): message = message.split(" ", 1) # if user sent: email bob@none.net # Hello Bob @@ -233,4 +227,3 @@ def handle_email(nodeID, message): return "Error: ⛔️ not understood. use:email bob@example.com # Hello Bob" - \ No newline at end of file diff --git a/modules/system.py b/modules/system.py index ee163a8..5f4955a 100644 --- a/modules/system.py +++ b/modules/system.py @@ -38,6 +38,12 @@ if motd_enabled: trap_list = trap_list + trap_list_motd help_message = help_message + ", motd" +# SMTP Configuration +if enableSMTP: + from modules.smtp import * # from the spudgunman/meshing-around repo + trap_list = trap_list + trap_list_smtp + help_message = help_message + ", email, sms" + # Emergency Responder Configuration if emergency_responder_enabled: trap_list_emergency = ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue")