spaeter – a partly vibed imap based very simple mail scheduler written in python
by Ulf Schleth

a vibed image for „spaeter“, a vibed mail scheduler
preface
hi. this time in intentional lowercase.
since ai is not good in programming, i started to vibe some little tools with my ai colleagues so they cant do wrong so much. nevertheless, vibing is work. like modeling an ashtray out of clay. or a statue out of soapstone. in this case, maybe more an ugly little cup. lets start with this: i cant count how often i was annoyed by the fact there is no standardized way to send a mail scheduled.
you know that feeling when you would like to be able to send yourself a reminder at a certain time via email, because your email awareness is much higher than your awareness for anything else? or its friday afternoon and you know when you write your email now, nobody would read it, but if you would send it on monday forenoon that would change? or you would like to send a mail to your colleague right after his holiday, because you know he is such a neoliberal stupid that he cant resist reading it in his holiday because he has no life and you want to help him get one?
i dont need that often, but if i need it, i need it urgently. i know thunderbird has a plugin which can do this but that only runs when thunderbird is running. and i dont use thunderbird. and i am changing workplaces all the time. since i used a nextcloud, i thought it would be a great thing to put something like that into it.
therefore, i filed a feature request for that. and you wont believe it, some years later they did start to work on that request. but after they finished – how disappointed i was when i noticed that i wouldnt use it, because when you install the nextcloud mail client you are not able to use it just to send mails. it forces you to receive your mails and does a lot of things to your email you dont want, like marking it as important or whatever.
spaeter
now, i decided to do a very simple solution called spaeter which means later in german. if you want to send a mail to a certain time you have to write the send time at the beginning of your subject like this:
2125-07-24 14:00 BUH! A mail by your dead grandfather
and save it to a configured imap folder. in evolution i do this by saving my mail to the drafts folder and moving it from there to the scheduling folder which i called spaeter. thats it. and the best is: it works for any client. to be fair, for any client which allows you to save a message into your schedule folder. you could even use your drafts folder for it if you are courageous enough. when the time has come, the email will be sent (without the time in the subject) and, if thats possible and if sent_folder is configured in the configuration file, moved to the sent folder (with the time).
you can configure as many accounts as you like but there is a catch: you have to store your passwords in plaintext. at least do a chmod 600 on the configuration file. if you find a way to encrypt the password in a simple way which isnt based on snakeoil (=“security by obscurity“), please leave a note in the comments.
requirements: you need some basic skills (since i dont explain everything in detail) and a computer to run this script.
that computer can be a server – whether it is a real server or just your own raspberry pi does not matter. the important thing is that the host running this script needs to be running continuously if you want to use it in real time. of course, you could also install it on your laptop; the script will still send out scheduled emails even after their planned time if it was not running earlier. you also need python and some modules. run: pip3 install –user pyyaml pytz and do the same for any other python components that might be missing.
installation: just copy both files into the same folder. execute the python script spaeter.py. edit the config file and save it as spaeter-config.yml – you dont have to fill out sent_folder – you wont get a sent copy then. so do it. and you can add account blocks or delete them if you for example need only one. try it out on the command line until it works. then do a crontab -e and add a line like this:
*/5 * * * * /usr/bin/python3 /home/user/bin/spaeter.py >> /dev/null 2>&1
you may also run it continuously in daemon mode, you just have to uncomment 2 lines, have a look at the end of the file.
finally
here is the source code. save it as spaeter.py – have fun:
#!/usr/bin/env python3 # # spaeter - an imap based mail scheduler, come and viisit: # https://www.schleth.com/tooltips/spaeter-a-partly-vibed-imap-based-very-simple-mail-scheduler-written-in-python-2605.html # import imaplib, smtplib, email, os from email.message import EmailMessage import yaml import time import ssl import pytz from datetime import datetime from email.header import decode_header # the config file should be in the same directory as this script CONFIG_FILE = 'spaeter-config.yml' # Decodes MIME-encoded words in an email header string to readable text def decode_mime_words(s): if s is None: return "" decoded = decode_header(s) return ''.join([str(t[0], t[1] or 'utf-8') if isinstance(t[0], bytes) else str(t[0]) for t in decoded]) # Extracts the scheduled send time from the start of the subject and returns it (in UTC) along with the cleaned subject # If extraction fails, returns None and the original subject def extract_send_time_and_clean_subject(subject): try: send_time_str = subject[:16] send_time = TZ.localize(datetime.strptime(send_time_str, "%Y-%m-%d %H:%M")).astimezone(pytz.utc) cleaned_subject = subject[16:].strip() return send_time, cleaned_subject except Exception: return None, subject # Extracts and cleans the body of an email message def extract_clean_body(msg): if msg.is_multipart(): for part in msg.walk(): if part.get_content_type() == "text/plain": return part.get_payload(decode=True).decode(part.get_content_charset() or 'utf-8').strip() else: return msg.get_payload(decode=True).decode(msg.get_content_charset() or 'utf-8').strip() return "" # Processes a single email account: checks for scheduled emails, and sends them if the scheduled time has come def process_account(account): now_utc = datetime.utcnow().replace(tzinfo=pytz.utc) try: mail = imaplib.IMAP4_SSL(account['imap']['server']) mail.login(account['imap']['user'], account['imap']['pass']) mail.select(account['imap']['folder']) typ, data = mail.search(None, 'ALL') for num in data[0].split(): typ, msg_data = mail.fetch(num, '(RFC822)') msg = email.message_from_bytes(msg_data[0][1]) send_time, cleaned_subject = extract_send_time_and_clean_subject(decode_mime_words(msg["Subject"])) if not send_time or now_utc < send_time: continue print(f"[{account['imap']['user']}] Send mail as planned at {send_time.astimezone(TZ)}") forward = EmailMessage() forward["From"] = msg["From"] forward["To"] = msg["To"] forward["Subject"] = cleaned_subject cleaned_body = extract_clean_body(msg) forward.set_content(cleaned_body) context = ssl.create_default_context() sent_success = False try: with smtplib.SMTP(account['smtp']['server'], 587) as s: s.starttls(context=context) s.login(account['smtp']['user'], account['smtp']['pass']) s.send_message(forward) sent_success = True except Exception as send_err: print(f"Failed to send mail: {send_err}") if sent_success: sent_folder = account['imap'].get('sent_folder', '').strip() if sent_folder: # Copy to sent folder and mark as deleted in current folder # uses imap & keeps the send-time in the subject for traceability and simplicity copy_result = mail.copy(num, sent_folder) if copy_result[0] == 'OK': mail.store(num, '+FLAGS', '\\Deleted') else: print(f"Failed to move mail to sent folder: {sent_folder}") else: mail.store(num, '+FLAGS', '\\Deleted') mail.expunge() mail.logout() except Exception as e: print(f"Error with account {account['imap']['user']}: {e}") def get_timezone_from_config(config): """ Returns the pytz timezone object from the loaded config dict. """ tzname = config.get('timezone', 'Europe/Berlin') return pytz.timezone(tzname) # Main function: loads configuration and processes each account def main(): script_dir = os.path.dirname(os.path.realpath(__file__)) config_path = os.path.join(script_dir, CONFIG_FILE) with open(config_path, 'r') as f: config = yaml.safe_load(f) global TZ TZ = get_timezone_from_config(config) # UNCOMMENT THE FOLLOWING LINE AND ADJUST IINDENTING TO ENABLE CONTINUOUS PROCESSING #while True: for account in config['accounts']: process_account(account) # UNCOMMENT THE FOLLOWING LINE AND ADJUST IINDENTING TO ENABLE CONTINUOUS PROCESSING # time.sleep(config.get('interval_seconds', 60)) if __name__ == '__main__': main()
and finally, here is the config file, save it as spaeter-config.yml and change it so it fits your needs:
timezone: Europe/Berlin interval_seconds: 60 #ignore this unless you use daemon mode accounts: - imap: server: imap1.mailfirma.de user: user1 pass: deinpass1 folder: spaeter sent_folder: Sent smtp: server: smtp.mailfirma.de user: user1 pass: deinpass1 - imap: server: imap2.mailfirma.de user: user2@firma.de pass: deinpass2 folder: spaeter sent_folder: Sent smtp: server: smtp.mailfirma.de user: user2@firma.de pass: deinpass2
Kommentar schreiben