import email import email.mime.application import email.mime.multipart import email.mime.text from email.header import decode_header, make_header import imaplib import os.path import smtplib import code128 import imapclient from bureau import Bureau, add_command, add_api def clean_header(header): """ Converts header string to unicode with HTML entities """ header = str(make_header(decode_header(header))) header = header.replace("<", "<") header = header.replace(">", ">") return header class Message(object): """ This is just a convenience class for holding email message data. It could be fancier some day but for now this seems fine. """ pass class MailRoom(Bureau): """ The Mail Room handles (electronic) post for the other Bureaus of the Screenless Office. """ name = "Mail Room" prefix = "PO" version = 0 def __init__(self): Bureau.__init__(self) # TODO: multiple accounts / folders if "user" in self.config: self.login = self.config["user"]["login"] self.password = self.config["user"]["password"] self.host = self.config["user"]["host"] self.spamfolder = self.config["user"]["spamfolder"] self.trashfolder = self.config["user"]["trashfolder"] self.imap_ssl = self.config["user"]["ssl"] if self.imap_ssl.lower() == "true": self.imap_ssl = True else: self.imap_ssl = False self.imapserv = imapclient.IMAPClient(self.host, use_uid=True, ssl=self.imap_ssl) self.imapserv.login(self.login, self.password) self.imapserv.select_folder("INBOX") else: print("you need to configure an IMAP account!") print("add a [user] section to PO.ini with:") print(" login = mylogin") print(" password = mypassword") print(" host = my.imap.server.address.com") # setup db's for mapping short codes to IMAP msg ids (and reversed) self.postdb = self.dbenv.open_db(b"postdb") self.postdb_rev = self.dbenv.open_db(b"postdb_rev") def _connect_imap(self): """ connect / reconnect to imap server """ try: self.imapserv.select_folder("INBOX") except self.imapserv.AbortError as err: self.log.debug("reconnecting after imap connection error: ", err) self.imapserv.logout() self.imapserv = imapclient.IMAPClient(self.host, use_uid=True, ssl=self.imap_ssl) self.imapserv.login(self.login, self.password) self.imapserv.select_folder("INBOX") def _make_msg_object(self, imap_id, resp_obj): """ util tidies up message headers deals with encoding """ internaldate = resp_obj[imap_id][b'INTERNALDATE'] msg_data = resp_obj[imap_id][b'RFC822'].decode('utf-8') msg_obj = email.message_from_string(msg_data) # format and tidy header data msg = Message() msg.fromstr = clean_header(msg_obj['From']) msg.tostr = clean_header(msg_obj['To']) msg.subject = clean_header(msg_obj['Subject']) if 'Cc' in msg_obj: msg.cc = clean_header(msg_obj['Cc']) else: msg.cc = None msg.date = internaldate msg.attachments = [] # TODO: deal with attachments # TODO: should use msg_obj.get_body and deal with HTML msg.content = "" # extract other sub-messages / attachments for part in msg_obj.walk(): # TODO: save interesting attachments to files # should clean these up on delete if part.get_content_type() == "text/plain": msg.content = part.get_payload(decode=True).decode("utf-8") msg.content = msg.content.replace("<", "<") msg.content = msg.content.replace(">", ">") msg.content = msg.content.replace("\n", "
") return msg def get_imap_id(self, msgid): """ take short code and look up the IMAP message id """ with self.dbenv.begin(db=self.postdb) as txn: imap_id = txn.get(msgid.encode()) return int(imap_id) def _imap2shortcode(self, msgid): """ returns a short code for a given IMAP id (creating a new mapping if needed) """ with self.dbenv.begin(db=self.postdb_rev) as txn: shortcode = txn.get(msgid.encode()) if shortcode is not None: return shortcode else: shortcode = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(5)) txn.put(msgid.encode(), shortcode.encode()) with self.dbenv.begin(db=self.postdb) as txn: txn.put(shortcode.encode(), msgid.encode()) return shortcode @add_command("fax", "Send a Document Camera Image via Email") def fax(self, data): """ Takes a photograph using the document camera and sends it in an E-mail. """ photo = self.send("PXphoto.") @add_command("r", "Print full email") def read(self, data): """ Prints out the full detailed version of an email. """ shortcode, _ = data.split(".") imap_id = self.get_imap_id(shortcode) self._connect_imap() resp = self.imapserv.fetch([imap_id], ['INTERNALDATE', 'RFC822']) msg = self._make_msg_object(imap_id, resp) # make action barcodes barcode_png = os.path.join("/tmp", "POun." + msg.shortcode + ".png") code128.image("POun." + shortcode).save(barcode_png) msg.un_bc = str(barcode_png) barcode_png = os.path.join("/tmp", "POd." + msg.shortcode + ".png") code128.image("POd." + shortcode).save(barcode_png) msg.d_bc = str(barcode_png) barcode_png = os.path.join("/tmp", "POsp." + msg.shortcode + ".png") code128.image("POsp." + shortcode).save(barcode_png) msg.sp_bc = str(barcode_png) barcode_png = os.path.join("/tmp", "POre." + msg.shortcode + ".png") code128.image("POre." + shortcode).save(barcode_png) msg.re_bc = str(barcode_png) barcode_png = os.path.join("/tmp", "POrea." + msg.shortcode + ".png") code128.image("POrea." + shortcode).save(barcode_png) msg.rea_bc = str(barcode_png) barcode_png = os.path.join("/tmp", "POr." + msg.shortcode + ".png") code128.image("POr." + shortcode).save(barcode_png) msg.r_bc = str(barcode_png) self.print_full("email.html", msg=msg, shortcode=shortcode) @add_command("d", "Delete email") def delete(self, data): """ Deletes an email and moves it to the trash folder. """ shortcode, _ = data.split(".") imap_id = self.get_imap_id(shortcode) self._connect_imap() self.imapserv.add_flags(imap_id, [imapclient.SEEN]) self.imapserv.copy((imap_id), self.trashfolder) self.imapserv.delete_messages((imap_id)) self.imapserv.expunge() @add_command("sp", "Mark as spam") def mark_spam(self, data): """ Flags an email as spam, mark as read and move it to the configured SPAM folder. """ shortcode, _ = data.split(".") imap_id = self.get_imap_id(shortcode) self._connect_imap() self.imapserv.add_flags(imap_id, [imapclient.SEEN]) self.imapserv.copy((imap_id), self.spamfolder) self.imapserv.delete_messages((imap_id)) self.imapserv.expunge() @add_command("un", "Mark as unread") def mark_unread(self, data): """ Flags an email as unseen (so you can deal with it later). """ shortcode, _ = data.split(".") imap_id = self.get_imap_id(shortcode) self._connect_imap() self.imapserv.remove_flags(imap_id, [imapclient.SEEN]) @add_command("re", "Reply with scan") def reply_scan(self, data): """ Reply to the sender of a mail with the PDF currently queued in the document scanner. """ # look up short code to get IMAP ID shortcode, _ = data.split(".") imap_id = self.get_imap_id(shortcode) self._connect_imap() # extract the sender and title resp = self.imapserv.fetch([imap_id], ['INTERNALDATE', 'RFC822']) internaldate = resp[imap_id][b'INTERNALDATE'] bodytext = """ This email is sent from a screenless office. If this is too wierd or awkward, just reply and let us find a more acceptable channel. http://screenl.es """ msg_data = resp[imap_id][b'RFC822'].decode('utf-8') msg_obj = email.message_from_string(msg_data) # put together the reply msg = email.mime.multipart.MIMEMultipart() msg["From"] = msg_obj["To"] # TODO: deal with ReplyTo headers properly msg["To"] = msg_obj["From"] msg["Subject"] = "Re: " + msg_obj["Subject"] #TODO: add reference headers from msg id for threading msg["Date"] = email.utils.formatdate(localtime=True) msg.attach(email.mime.text.MIMEText(bodytext)) # attach scanned image photo = self.send("PX", "photo")["photo"] with open(photo, "rb") as fil: base = os.path.basename(photo) part = email.mime.application.MIMEApplication(fil.read(), Name=base) part['Content-Disposition'] = 'attachment; filename="%s"' % base msg.attach(part) # send SMTP smtp = smtplib.SMTP(self.host, 587) smtp.starttls() smtp.login(self.login, self.password) smtp.sendmail(msg["From"], msg["To"], msg.as_string()) smtp.close() # flag as replied self.imapserv.add_flags(imap_id, [imapclient.ANSWERED]) @add_api("unread", "Get unread mails") def unread(self): """ Polls the currently configured IMAP server and returns a dict containing unread emails. """ self._connect_imap() messages = self.imapserv.sort("ARRIVAL", ["UNSEEN"]) print("%d unread messages in INBOX" % len(messages)) resp = self.imapserv.fetch(messages, ['FLAGS', 'INTERNALDATE', 'ENVELOPE', 'RFC822.SIZE']) # massage into a serializable dict msgs = [] for msgid, data in resp.items(): msg = self._make_msg_object(msgid, data) shortcode = self._imap2shortcode(msgid) # make action barcodes msg.d_bc = code128.svg("POd." + shortcode).encode() msg.sp_bc = code128.svg("POsp." + shortcode).encode() msg.r_bc = code128.svg("POr." + shortcode).encode() msgs.append(msg) return msgs def main(): mr = MailRoom() mr.run() if __name__ == "__main__": main()