def send_text(self, text, envelop: Envelop): msg = Message() msg.set_payload(text) msg.add_header('Content-Type', 'text/html') return self.send(msg, envelop)
def sendMessage(self, From, To, Subj, extrahdrs, bodytext, attaches, saveMailSeparator=(('=' * 80) + 'PY\n'), bodytextEncoding='us-ascii', attachesEncodings=None): """ formats and sends an e-mail. saves the sent e-mail if sent sucessfully. arguments: bodytext - text part of the e-mail (assumes it is already in desired encoding.) attaches - list of files to be attached extrahdrs - list of tuples to be added (Name, Value) """ # body text encoding if fix_text_required(bodytextEncoding): if not isinstance(bodytext, str): bodytext = bodytext.decode(bodytextEncoding) else: if not isinstance(bodytext, bytes): bodytext = bodytext.encode(bodytextEncoding) # attachments if not attaches: msg = Message() msg.set_payload(bodytext, charset=bodytextEncoding) else: msg = MIMEMultipart() self.addAttachments(msg, bodytext, attaches, bodytextEncoding, attachesEncodings) # e-mail header encoding hdrenc = mailconfig.headersEncodeTo or 'utf-8' Subj = self.encodeHeader(Subj, hdrenc) From = self.encodeAddrHeader(From, hdrenc) To = [self.encodeAddrHeader(T, hdrenc) for T in To] Tos = ', '.join(To) # attach header to message msg['From'] = From msg['To'] = Tos msg['Subj'] = Subj msg['Date'] = email.utils.formatdate() recip = To for (name, value) in extrahdrs: if value: if name.lower() not in ['cc', 'bcc']: value = self.encodeHeader(value, hdrenc) msg[name] = value else: value = [self.encodeAddrHeader(V, hdrenc) for V in value] recip += value if name.lower() != 'bcc': msg[name] = ', '.join(value) # remove duplicates recip = list(set(recip)) fullText = msg.as_string() self.trace('Sending to...' + str(recip)) self.trace(fullText[:self.tracesize]) # smtp connection. server = smtplib.SMTP_SSL(self.smtpservername) self.getPassword() self.authenticateServer(server) try: failed = server.sendmail(From, recip, fullText) except Exception: server.close() raise else: server.quit() self.saveSentMessage(fullText, saveMailSeparator) if failed: class SomeAddrsFailed(Exception): pass raise SomeAddrsFailed('Failed addrs:%s\n' % failed) self.trace('Send exit')
def insert(self, table, fields): def add_payload(message, obj): payload = Message() encoding = obj.get("encoding", "utf-8") if encoding and (encoding.upper() in ("BASE64", "7BIT", "8BIT", "BINARY")): payload.add_header("Content-Transfer-Encoding", encoding) else: payload.set_charset(encoding) mime = obj.get("mime", None) if mime: payload.set_type(mime) if "text" in obj: payload.set_payload(obj["text"]) elif "payload" in obj: payload.set_payload(obj["payload"]) if "filename" in obj and obj["filename"]: payload.add_header("Content-Disposition", "attachment", filename=obj["filename"]) message.attach(payload) mailbox = table.mailbox d = dict(((k.name, v) for k, v in fields)) date_time = d.get("created") or datetime.datetime.now() struct_time = date_time.timetuple() if len(d) > 0: message = d.get("email", None) attachments = d.get("attachments", []) content = d.get("content", []) flags = " ".join([ "\\%s" % flag.capitalize() for flag in ("answered", "deleted", "draft", "flagged", "recent", "seen") if d.get(flag, False) ]) if not message: from email.message import Message mime = d.get("mime", None) charset = d.get("encoding", None) message = Message() message["from"] = d.get("sender", "") message["subject"] = d.get("subject", "") message["date"] = self.convert_date(date_time, imf=True) if mime: message.set_type(mime) if charset: message.set_charset(charset) for item in ("to", "cc", "bcc"): value = d.get(item, "") if isinstance(value, basestring): message[item] = value else: message[item] = ";".join([i for i in value]) if (not message.is_multipart() and (not message.get_content_type().startswith("multipart"))): if isinstance(content, basestring): message.set_payload(content) elif len(content) > 0: message.set_payload(content[0]["text"]) else: [add_payload(message, c) for c in content] [add_payload(message, a) for a in attachments] message = message.as_string() result, data = self.connection.append(mailbox, flags, struct_time, message) if result == "OK": uid = int(re.findall("\d+", str(data))[-1]) return self.db(table.uid == uid).select(table.id).first().id else: raise Exception("IMAP message append failed: %s" % data) else: raise NotImplementedError("IMAP empty insert is not implemented")
def test_get_activities_fetch_extras(self): self.init() # Generate minimal fake responses for each request in the batch. # # Test with multiple activities to cover the bug described in # https://github.com/snarfed/bridgy/issues/22#issuecomment-56329848 : # util.CacheDict.get_multi() didn't originally handle generator args. batch = MIMEMultipart() for i, item in enumerate((COMMENT_GP, PLUSONER, RESHARER) * 2): msg = Message() msg.set_payload('HTTP/1.1 200 OK\n\r\n\r\n' + json.dumps({'items': [item]})) msg['Content-ID'] = '<response-abc+%d>' % (i + 1) batch.attach(msg) # as_string() must be called before get_boundary() to generate the # boundaries between parts, but can't be called again, so we capture the # result. batch_str = batch.as_string() gpe_1 = ACTIVITY_GP_EXTRAS gpe_2 = copy.deepcopy(gpe_1) gpe_2['id'] = '002' http_seq = http.HttpMockSequence([ ({ 'status': '200' }, json.dumps({'items': [gpe_1, gpe_2]})), ({ 'status': '200', 'content-type': 'multipart/mixed; boundary="%s"' % batch.get_boundary() }, batch_str), ({ 'status': '200' }, json.dumps({'items': [gpe_1, gpe_2]})), ]) self.auth_entity.http = lambda: http_seq ase_1 = ACTIVITY_AS_EXTRAS ase_2 = copy.deepcopy(ase_1) ase_2['id'] = tag_uri('002') ase_2['object']['tags'][0]['id'] = tag_uri('002_liked_by_222') ase_2['object']['tags'][1]['id'] = tag_uri('002_shared_by_444') cache = util.CacheDict() self.assert_equals([ase_1, ase_2], self.googleplus.get_activities(fetch_replies=True, fetch_likes=True, fetch_shares=True, cache=cache)) for id in '001', '002': for prefix in 'AGL ', 'AGS ': self.assertEquals(1, cache[prefix + id]) # no new extras, so another request won't fill them in as_1 = copy.deepcopy(ACTIVITY_AS) for field in 'replies', 'plusoners', 'resharers': as_1['object'][field] = {'totalItems': 1} as_2 = copy.deepcopy(as_1) as_2['id'] = tag_uri('002') self.assert_equals([as_1, as_2], self.googleplus.get_activities(fetch_replies=True, fetch_likes=True, fetch_shares=True, cache=cache))
#!/usr/bin/env python3 # Foundations of Python Network Programming, Third Edition # https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter12/pre-python-3.4/trad_gen_newhdrs.py # Traditional Message Generation with Date and Message-ID import email.utils from email.message import Message message = """Hello, This is a test message from Chapter 12. I hope you enjoy it! -- Anonymous""" msg = Message() msg['To'] = '*****@*****.**' msg['From'] = 'Test Sender <*****@*****.**>' msg['Subject'] = 'Test Message, Chapter 12' msg['Date'] = email.utils.formatdate(localtime=1) msg['Message-ID'] = email.utils.make_msgid() msg.set_payload(message) print(msg.as_string())
def _getMessage(ctx, message): COMMASPACE = ', ' sendermail = message.SenderAddress sendername = message.SenderName subject = message.Subject if isDebugMode(): msg = getMessage(ctx, g_message, 251, subject) logMessage(ctx, INFO, msg, 'SmtpService', 'sendMailMessage()') textmsg = Message() content = message.Body flavors = content.getTransferDataFlavors() #Use first flavor that's sane for an email body for flavor in flavors: if flavor.MimeType.find('text/html') != -1 or flavor.MimeType.find( 'text/plain') != -1: textbody = content.getTransferData(flavor) if len(textbody): mimeEncoding = re.sub('charset=.*', 'charset=UTF-8', flavor.MimeType) if mimeEncoding.find('charset=UTF-8') == -1: mimeEncoding = mimeEncoding + '; charset=UTF-8' textmsg['Content-Type'] = mimeEncoding textmsg['MIME-Version'] = '1.0' try: #it's a string, get it as utf-8 bytes textbody = textbody.encode('utf-8') except: #it's a bytesequence, get raw bytes textbody = textbody.value if sys.version >= '3': if sys.version_info.minor < 3 or ( sys.version_info.minor == 3 and sys.version_info.micro <= 1): #http://stackoverflow.com/questions/9403265/how-do-i-use-python-3-2-email-module-to-send-unicode-messages-encoded-in-utf-8-w #see http://bugs.python.org/16564, etc. basically it now *seems* to be all ok #in python 3.3.2 onwards, but a little busted in 3.3.0 textbody = textbody.decode('iso8859-1') else: textbody = textbody.decode('utf-8') c = Charset('utf-8') c.body_encoding = QP textmsg.set_payload(textbody, c) else: textmsg.set_payload(textbody) break if message.hasAttachments(): msg = MIMEMultipart() msg.epilogue = '' msg.attach(textmsg) else: msg = textmsg header = Header(sendername, 'utf-8') header.append('<' + sendermail + '>', 'us-ascii') msg['Subject'] = subject msg['From'] = header msg['To'] = COMMASPACE.join(message.getRecipients()) msg['Message-ID'] = message.MessageId if message.ThreadId: msg['References'] = message.ThreadId if message.hasCcRecipients(): msg['Cc'] = COMMASPACE.join(message.getCcRecipients()) if message.ReplyToAddress != '': msg['Reply-To'] = message.ReplyToAddress xmailer = "LibreOffice / OpenOffice via smtpMailerOOo extention" try: configuration = getConfiguration(ctx, '/org.openoffice.Setup/Product') name = configuration.getByName('ooName') version = configuration.getByName('ooSetupVersion') xmailer = "%s %s via smtpMailerOOo extention" % (name, version) except: pass msg['X-Mailer'] = xmailer msg['Date'] = formatdate(localtime=True) for attachment in message.getAttachments(): content = attachment.Data flavors = content.getTransferDataFlavors() flavor = flavors[0] ctype = flavor.MimeType maintype, subtype = ctype.split('/', 1) msgattachment = MIMEBase(maintype, subtype) data = content.getTransferData(flavor) msgattachment.set_payload(data.value) encode_base64(msgattachment) fname = attachment.ReadableName try: msgattachment.add_header('Content-Disposition', 'attachment', \ filename=fname) except: msgattachment.add_header('Content-Disposition', 'attachment', \ filename=('utf-8','',fname)) msg.attach(msgattachment) return msg
def sendMailMessage(self, xMailMessage): COMMASPACE = ', ' if dbg: print("PyMailSMTPService sendMailMessage", file=dbgout) recipients = xMailMessage.getRecipients() sendermail = xMailMessage.SenderAddress sendername = xMailMessage.SenderName subject = xMailMessage.Subject ccrecipients = xMailMessage.getCcRecipients() bccrecipients = xMailMessage.getBccRecipients() if dbg: print("PyMailSMTPService subject: " + subject, file=dbgout) print("PyMailSMTPService from: " + sendername, file=dbgout) print("PyMailSMTPService from: " + sendermail, file=dbgout) print("PyMailSMTPService send to: %s" % (recipients, ), file=dbgout) attachments = xMailMessage.getAttachments() textmsg = Message() content = xMailMessage.Body flavors = content.getTransferDataFlavors() if dbg: print("PyMailSMTPService flavors len: %d" % (len(flavors), ), file=dbgout) #Use first flavor that's sane for an email body for flavor in flavors: if flavor.MimeType.find('text/html') != -1 or flavor.MimeType.find( 'text/plain') != -1: if dbg: print("PyMailSMTPService mimetype is: " + flavor.MimeType, file=dbgout) textbody = content.getTransferData(flavor) if len(textbody): mimeEncoding = re.sub("charset=.*", "charset=UTF-8", flavor.MimeType) if mimeEncoding.find('charset=UTF-8') == -1: mimeEncoding = mimeEncoding + "; charset=UTF-8" textmsg['Content-Type'] = mimeEncoding textmsg['MIME-Version'] = '1.0' try: #it's a string, get it as utf-8 bytes textbody = textbody.encode('utf-8') except: #it's a bytesequence, get raw bytes textbody = textbody.value if sys.version >= '3': if sys.version_info.minor < 3 or ( sys.version_info.minor == 3 and sys.version_info.micro <= 1): #http://stackoverflow.com/questions/9403265/how-do-i-use-python-3-2-email-module-to-send-unicode-messages-encoded-in-utf-8-w #see http://bugs.python.org/16564, etc. basically it now *seems* to be all ok #in python 3.3.2 onwards, but a little busted in 3.3.0 textbody = textbody.decode('iso8859-1') else: textbody = textbody.decode('utf-8') c = Charset('utf-8') c.body_encoding = QP textmsg.set_payload(textbody, c) else: textmsg.set_payload(textbody) break if (len(attachments)): msg = MIMEMultipart() msg.epilogue = '' msg.attach(textmsg) else: msg = textmsg hdr = Header(sendername, 'utf-8') hdr.append('<' + sendermail + '>', 'us-ascii') msg['Subject'] = subject msg['From'] = hdr msg['To'] = COMMASPACE.join(recipients) if len(ccrecipients): msg['Cc'] = COMMASPACE.join(ccrecipients) if xMailMessage.ReplyToAddress != '': msg['Reply-To'] = xMailMessage.ReplyToAddress mailerstring = "LibreOffice via Caolan's mailmerge component" try: ctx = uno.getComponentContext() aConfigProvider = ctx.ServiceManager.createInstance( "com.sun.star.configuration.ConfigurationProvider") prop = uno.createUnoStruct('com.sun.star.beans.PropertyValue') prop.Name = "nodepath" prop.Value = "/org.openoffice.Setup/Product" aSettings = aConfigProvider.createInstanceWithArguments( "com.sun.star.configuration.ConfigurationAccess", (prop, )) mailerstring = aSettings.getByName("ooName") + " " + \ aSettings.getByName("ooSetupVersion") + " via Caolan's mailmerge component" except: pass msg['X-Mailer'] = mailerstring msg['Date'] = formatdate(localtime=True) for attachment in attachments: content = attachment.Data flavors = content.getTransferDataFlavors() flavor = flavors[0] ctype = flavor.MimeType maintype, subtype = ctype.split('/', 1) msgattachment = MIMEBase(maintype, subtype) data = content.getTransferData(flavor) msgattachment.set_payload(data.value) encode_base64(msgattachment) fname = attachment.ReadableName try: msgattachment.add_header('Content-Disposition', 'attachment', \ filename=fname) except: msgattachment.add_header('Content-Disposition', 'attachment', \ filename=('utf-8','',fname)) if dbg: print(("PyMailSMTPService attachmentheader: ", str(msgattachment)), file=dbgout) msg.attach(msgattachment) uniquer = {} for key in recipients: uniquer[key] = True if len(ccrecipients): for key in ccrecipients: uniquer[key] = True if len(bccrecipients): for key in bccrecipients: uniquer[key] = True truerecipients = uniquer.keys() if dbg: print(("PyMailSMTPService recipients are: ", truerecipients), file=dbgout) self.server.sendmail(sendermail, truerecipients, msg.as_string())
class MailParser(MailTool): """ 解析消息文本和附件的方法 微妙细节:对于非多组分的消息,Message对象负载是一个简单的字符串,对于多组分消息,它 是Message对象组成的列表(可能拥有嵌套结构),这里我们无须区别对待这两种情况,因为Message 遍历生成器总是先返回自身,因此对于非多组分消息也可以运行无误(遍历了单个对象); 对于简单消息,这里总是把消息看成邮件的一部分;对于多组分消息,组分列表包含主体消息文本 及所有附件;这样使得不属于类型文本的简单消息在UI中当作附件来处理(如保存,打开);对于 某些不常见的组分类型,Message负载也有可能为None; Py3.1中,使用decode=1时将文本组分的负载作为bytes返回,否则肯恩是str;在mailtools中,文件 保存时将文本存储为bytes,而主文本字节负载则依照邮件题头信息或操作平台默认值及推测结果 解码为Unicode str;客户端可能需要转换其它负载:PyMailGUI利用题头解码保存到二进制文件的组 分; 支持基于抓取来的消息内容的自动解码,包括Subject等厂家题头及From和To等地质题头中的姓名: 客户端必须在解析后请求此操作;解析器在显示之前不进行解码; """ def walkNamedParts(self, message): """ 为避免重复组分命名逻辑的生成器;跳过多组分消息的题头,生成组分文件名;消息 已经在解析好的email.message.Message对象中;不跳过罕见类型:负载可能为None, 必须在组分保存时予以处理;某些其它情况可能也可以跳过; """ for (ix, part) in enumerate(message.walk()): # 遍历包括消息 fulltype = part.get_content_type() # ix包括跳过的组分 maintype = part.get_content_maintype() if maintype == 'multipart': # multipart/*:容器 continue elif fulltype == 'message/rfc822': # 跳过message/rfc822 continue # 跳过所有message/*? else: filename, contype = self.partName(part, ix) yield filename, contype, part def partName(self, part, ix): """ 从消息组分中提取文件名和内容类型; 文件名:尝试Content-Disposition名称参数,然后尝试Content-Type。或者根据 mimetype推测结果生成文件名; """ filename = part.get_filename() # 文件名在消息题头中? contype = part.get_content_type() # 主类/亚类为小写 if not filename: filename = part.get_param('name') # 尝试content-type名 if not filename: if contype == 'text/plain': # 硬编码纯文本后缀名 ext = '.txt' # 不然就猜它是.ksh! else: ext = mimetypes.guess_extension(contype) if not ext: # 使用通用默认设置 ext = '.bin' filename = 'part-%03d%s' % (ix, ext) return self.decodeHeader(filename), contype def saveParts(self, savedir, message): """ 将消息的所有部分保存为本地目录下的多个文件; 返回(['maintype/subtype', 'filename'])列表以供调用函数使用,不过这里不打开任何 组分或附件; get_paykiad解码base64、quoted-printable和uuencoding编码的数据; 对于罕见类型,邮件解析器可能给我们返回一个None负载,大概应该跳过:这里为了保险 将其转换为str; """ if not os.path.exists(savedir): os.mkdir(savedir) partfiles = [] for (filename, contype, part) in self.walkNamedParts(message): fullname = os.path.join(savedir, filename) fileobj = open(fullname, 'wb') # 使用二进制模式 content = part.get_payload(decode=1) # 解码base64、quoted-printable和uuencoding编码 if not isinstance(contype, bytes): # 打开文件的rb参数需要bytes content = b'(no content)' # decode=1返回bytes fileobj.write(content) # 不过某些负载返回None fileobj.close() # 不采用str(content) partfiles.append((contype, fullname)) # 供调用函数打开 return partfiles def saveOnePart(self, savedir, partname, message): """ 同上,不过只根据名称查找和保存一个部分 """ if not os.path.exists(savedir): os.mkdir(savedir) fullname = os.path.join(savedir, partname) (contype, content) = self.findOnePart(partname, message) if not isinstance(content, bytes): # 打开文件的rb参数需要bytes content = b'(no content)' # decode=1返回bytes open(fullname, 'wb').write(content) # 不过某些负载返回bytes return contype, fullname # 不采用str(content) def partsList(self, message): """ 返回列表,由已解析的消息的所有部分的文件名组成,和saveParts使用相同文件名 逻辑,不过这里不保存组分文件 """ validParts = self.walkNamedParts(message) return [filename for (filename, contype, part) in validParts] def findOnePart(self, partname, message): """ 查找并返回给定名称的组分的内容; 用来与partsList分配使用; 在这里我们也可以采用mimetypes.guess_type(partname); 我们还能通过保存到字典而避免这个搜索操作; 内容可能为str或bytes ---- 必要时转换; """ for (filename, contype, part) in self.walkNamedParts(message): if filename == partname: content = part.get_payload(decode=1) # 解码base64,quoted-printable,uuencoding return contype, content # 可能为bytes文本 def decodePayload(self, part, asStr=True): """ 将文本组分bytes解码为Unicode str,以便显示、自动换行等;组分是一个Message 对象,(decode=1)则取消MIME邮件编码(base64,quoted-printable和uuencoding编码), bytes.decode()进行额外的Unicode文本字符串解码;首先尝试消息题头中的字符集编码 名称(如果有且正确),然后尝试操作平台默认值及尝试推测数次,最后放弃,返回错误 字符串。 """ payload = part.get_payload(decode=1) # 负载可能是bytes if asStr and isinstance(payload, bytes): # decode=1返回bytes tries = [] enchdr = part.get_content_charset() # 想尝试消息题头 if enchdr: tries += [enchdr] # 想尝试题头 tries += [sys.getdefaultencoding()] # 相当于bytes.decode() tries += ['latin1', 'utf8'] # 尝试8比特编码,包括ascii for t in tries: # 尝试utf8 try: payload = payload.decode(t) # 试一试 break except (UnicodeError, LookupError): # LookupError: 不好的名字 pass else: payload = '--sorry: cannot decode Unicode text--' return payload def findMainText(self, message, asStr=True): """ 对于面向对文本的客户端,返回首个文本组分的str,对于简单消息的负载或多组分消息 的所有部分,查找text/plain,然后查找text/html,接着查找text/*,最后断定没有文 本可显示;这个操作时一个试探过程,不过能应付大多数简单的、multipart/alternative 和multipart/mixed信息;如果简单消息中没有,则content-type默认为text/plain; 通过遍历而非列表扫描处理最高层次的嵌套;如果是非多组分消息但类型为text/html, 返回HTML,因为文本带有HTML类型,调用函数可能用网页浏览器打开,提取纯文本,等 等,如果是非多组分消息而且不是文本,那么没有文本可显示:在UI中保存/打开消息内 容,缺陷:没有尝试将类型的text/plain的部分的数行连接在一起; 文本负载可能为bytes ---- 在此解码为str; 保存HTML文件时使用asStr=False以获取原始字节 """ # 尝试查找纯文本 for part in message.walk(): # 遍历访问消息 type = part.get_content_type() # 如果是非多组分消息 if type == 'text/plain': # 可能是base64,quoted-printable,uuencoding return type, self.decodePayload(part, asStr) # 也进行bytes到str的转换? # 尝试查找HTML组分 for part in message.walk(): type = part.get_content_type() # 调用函数渲染html if type == 'text/html': return type, self.decodePayload(part, asStr) # 尝试任何其他类型,包括XML for part in message.walk(): maintype = part.get_content_maintype() type = part.get_content_type() if maintype == 'text': return type, self.decodePayload(part, asStr) # 赌博做法:可以使用首个组分,不过它还没有被标记为文本 failtext = '[No text to display]' if asStr else b'[No text to display]' return 'text/plain', failtext def decodeHeader(self, rawheader): """ 根据其内容,依照电子邮件和Unicode标准解码已有的国际化题头文本;如果未被 编码或解码失败,按原样返回; 客户端必须调用这个函数来显示信息:解析得到Message对象不进行解码; 国际化题头示例: '=?UTF-8?Q?Introducing=20Top=20Values=20..Savers?='; 国际化题头示例: 'Man where did you get that =?UTF-8?Q?assistant=3F?='; decode_header自动处理题头字符串中的任何换行,如果题头的任何子字符串被编码, 则可能返回多个组分,如果找到任何编码名称则在组分列表中返回所有bytes(未编码组分 编码为raw-unicode-escape格式且enc=None),然而如果整个题头均未经过编码,则在enc=None 时在Py3.1中返回单独一个部分,类型为str而非bytes(这里必须对混合类型做处理); 以下代码属于最初的尝试,只要没有经过编码的子字符串,或者enc作为None返回(抛出异常, 返回做改变的原始题头),那么可以成功运行: hdr, enc = email.header.decode_header(rawheader)[0] return hrd.decode(enc) # 如果enc=None则失败:没有编码名称或经过编码的子字符串 """ try: parts = email.header.decode_header(rawheader) decoded = [] for (part, enc) in parts: # 对于所有子字符串 if enc is None: # 组分未经编码? if not isinstance(part, bytes): # str: 整个题头未经比啊那么 decoded += [part] # 否则进行unicode解码 else: decoded += [part.decode('raw-unicode-escape')] else: decoded += [part.decode(enc)] return ' '.join(decoded) except: return rawheader # 搏一把! def decodeAddrHeader(self, rawheader): """ 根据其内容,依照电子邮件和Unicode标准解码已有的国际化题头文本;必须解析电子邮件 地址的第一个部分以获取国际化部分:'"=?UTF-8?Walmart?="<*****@*****.**>'; From大概只有一个地址,不过To、CC和Bcc可能有多个; decodeHeader处理完整题头内嵌套着编码过的子字节串,不过这里我们不能简单地对于整个 题头调用这个函数,因为如果编码后的名称字符串以"引号而非泛空格符或首位字符串结尾的 话,这个函数将允许失败;mailSender模块中的encodeAddrHeader是这个函数的逆向操作;" 以下代码属于最初的尝试,如果存在编码过的子字符串,将在处理姓名中的编码过的子字符 串时失败,并针对未编码的bytes部分抛出异常; namebytes, nameenc = email.header.decode_header(name)[0] (do email+MIME) if nameenc: name = namebytes.decode(nameenc) (do Unicode?) """ try: pairs = email.utils.getaddresses([rawheader]) # 分割地址和组分 decoded = [] # 处理姓名中的逗号 for (name, addr) in pairs: try: name = self.decodeHeader(name) # 电子邮件+MIME+Unicode except: name = None # 如果decodeHeader抛出异常则使用编码过的姓名 joined = email.utils.formataddr((name, addr)) # 合并各部分 decoded.append(joined) return ', '.join(decoded) # >= 1地址 except: return self.decodeHeader(rawheader) # 尝试解码整个字符串 def splitAddresses(self, field): """ 在UI中对于多个地址使用逗号分隔符,使用getaddresses来正确进行分割并允许 地址的姓名部分中使用逗号,PyMailGUI在必要时使用它来分割由用户输入和题头 复制得到的收件人、抄送、密送;如果域为空或碰到了一场,则返回空列表; """ try: pairs = email.utils.getaddresses([field]) # [(姓名,地址)] return [email.utils.formataddr(pair) for pair in pairs] # [(姓名,地址)] except: return '' # 用户输入域中句法错误?或者其它错误 # 解析失败时的返回 errorMessage = Message() errorMessage.set_payload('[Unalbe to parse message - format error]') def parseHeaders(self, mailtext): """ 仅解析题头,返回email.message.Message根对象,在题头解析后停止,即便后面 没有东西(top)email.message.Message对象包含邮件题头域的映射关系消息对象的 负载为None,而非原始主体文本; """ try: return email.parser.Parser().parsestr(mailtext, headersonly=True) except: return self.errorMessage def parseMessage(self, fulltext): """ 解析整个消息,返回email.message.Message根对象,如果不采用is_multipart(), 消息对象的负载是一个字符串,如果含有多个组分,那么消息对象的负载是其它Message 对象,这个调用相当于email.message_from_string() """ try: return email.parser.Parser().parsestr(fulltext) # 可能失败! except: return self.errorMessage # 或者让调用函数来处理?可检查返回值 def parseMessageRaw(self, fulltext): """ 只解析题头,返回email.message.Message根对象 出于运行效率考虑,在题头解析后停止(这里还没用上)消息对象 的负载是题头之后的邮件原始文本 """ try: return email.parser.HeaderParser().parsestr(fulltext) except: return self.errorMessage
def emailException(self, htmlErrMsg): """Email the exception. Send the exception via mail, either as an attachment, or as the body of the mail. """ message = Message() # Construct the message headers headers = self.setting('ErrorEmailHeaders').copy() headers['Date'] = formatdate() headers['Mime-Version'] = '1.0' headers['Subject'] = '{} {}: {}'.format( headers.get('Subject', '[Webware Error]'), *sys.exc_info()[:2]) for header, value in headers.items(): if isinstance(value, (list, tuple)): value = ','.join(value) message.add_header(header, value) # Construct the message body if self.setting('EmailErrorReportAsAttachment'): # start off with a text/plain part text = self._emailBody.format( path=self.servletPathname(), ascTime=asclocaltime(self._time), time=self._time) message.set_type('multipart/mixed') part = Message() part.set_type('text/plain') body = StringIO() body.write(text) traceback.print_exc(file=body) part.set_payload(body.getvalue()) body.close() message.attach(part) part = Message() # now add htmlErrMsg part.add_header('Content-Transfer-Encoding', '7bit') part.add_header( 'Content-Description', 'HTML version of Webware error message') part.add_header( 'Content-Disposition', 'attachment', filename='WebwareErrorMsg.html') part.set_type('text/html') part.set_payload(htmlErrMsg) message.attach(part) else: message.set_type('text/html') message.set_payload(htmlErrMsg, 'us-ascii') # Send the message server = self.setting('ErrorEmailServer') # This setting can be: server, server:port, server:port:user:password # or server:port:user:password:popserver:popport for "smtp after pop". parts = server.split(':', 5) server = port = user = passwd = None popserver = popssl = popport = None try: # fetch individual parts until we get an IndexError server = parts[0] try: port = int(parts[1]) except ValueError: pass user = parts[2] passwd = parts[3] popserver = parts[4] try: popport = int(parts[5]) except ValueError: popport = None if parts[6].lower() == 'ssl': popssl = True except IndexError: pass if user and passwd and popserver: # SMTP after POP if popssl is None and popport == 995: popssl = True popssl = poplib.POP3_SSL if popssl else poplib.POP3 if popport: popserver = popssl(popserver, popport) else: popserver = popssl(popserver) popserver.set_debuglevel(0) popserver.user(user) popserver.pass_(passwd) try: popserver.quit() except Exception: pass if port: server = smtplib.SMTP(server, port) else: server = smtplib.SMTP(server) try: server.set_debuglevel(0) if user and passwd and not popserver: # SMTP-AUTH server.ehlo() if server.has_extn('starttls'): server.starttls() server.ehlo() server.login(user, passwd) body = message.as_string() server.sendmail(headers['From'], headers['To'], body) finally: try: server.quit() except Exception: pass
def test_prepare_multipart(): fixture_boundary = b'===============3996062709511591449==' here = os.path.dirname(__file__) multipart = os.path.join(here, 'fixtures/multipart.txt') client_cert = os.path.join(here, 'fixtures/client.pem') client_key = os.path.join(here, 'fixtures/client.key') client_txt = os.path.join(here, 'fixtures/client.txt') fields = { 'form_field_1': 'form_value_1', 'form_field_2': { 'content': 'form_value_2', }, 'form_field_3': { 'content': '<html></html>', 'mime_type': 'text/html', }, 'form_field_4': { 'content': '{"foo": "bar"}', 'mime_type': 'application/json', }, 'file1': { 'content': 'file_content_1', 'filename': 'fake_file1.txt', }, 'file2': { 'content': '<html></html>', 'mime_type': 'text/html', 'filename': 'fake_file2.html', }, 'file3': { 'content': '{"foo": "bar"}', 'mime_type': 'application/json', 'filename': 'fake_file3.json', }, 'file4': { 'filename': client_cert, 'mime_type': 'text/plain', }, 'file5': { 'filename': client_key, }, 'file6': { 'filename': client_txt, }, } content_type, b_data = prepare_multipart(fields) headers = Message() headers['Content-Type'] = content_type assert headers.get_content_type() == 'multipart/form-data' boundary = headers.get_boundary() assert boundary is not None with open(multipart, 'rb') as f: b_expected = f.read().replace(fixture_boundary, boundary.encode()) # Depending on Python version, there may or may not be a trailing newline assert b_data.rstrip(b'\r\n') == b_expected.rstrip(b'\r\n')
def _makeMessage(self): from email.message import Message return Message()
def _create_message(self): """Return basic instance of Message.""" self.msg = Message()
def apply(self, ui): # get message to forward if not given in constructor if not self.message: self.message = ui.current_buffer.get_selected_message() mail = self.message.get_email() envelope = Envelope() if self.inline: # inline mode # set body text name, address = self.message.get_author() timestamp = self.message.get_date() qf = settings.get_hook('forward_prefix') if qf: quote = qf(name, address, timestamp, ui=ui, dbm=ui.dbman) else: quote = 'Forwarded message from %s (%s):\n' % (name or address, timestamp) mailcontent = quote quotehook = settings.get_hook('text_quote') if quotehook: mailcontent += quotehook(self.message.accumulate_body()) else: quote_prefix = settings.get('quote_prefix') for line in self.message.accumulate_body().splitlines(): mailcontent += quote_prefix + line + '\n' envelope.body = mailcontent for a in self.message.get_attachments(): envelope.attach(a) else: # attach original mode # attach original msg original_mail = Message() original_mail.set_type('message/rfc822') original_mail['Content-Disposition'] = 'attachment' original_mail.set_payload(email_as_string(mail)) envelope.attach(Attachment(original_mail)) # copy subject subject = decode_header(mail.get('Subject', '')) subject = 'Fwd: ' + subject forward_subject_hook = settings.get_hook('forward_subject') if forward_subject_hook: subject = forward_subject_hook(subject) else: fsp = settings.get('forward_subject_prefix') if not subject.startswith(('Fwd:', fsp)): subject = fsp + subject envelope.add('Subject', subject) # set From-header and sending account try: from_header, _ = determine_sender(mail, 'reply') except AssertionError as e: ui.notify(str(e), priority='error') return envelope.add('From', from_header) # continue to compose ui.apply_command( ComposeCommand(envelope=envelope, spawn=self.force_spawn))
def __init__( self, formal_name=None, app_id=None, app_name=None, id=None, icon=None, author=None, version=None, home_page=None, description=None, startup=None, on_exit=None, factory=None, ): # Keep an accessible copy of the app instance App.app = self # We need a module name to load app metadata. If an app_name has been # provided, we can set the app name now, and derive the module name # from there. if app_name: self._app_name = app_name else: # If the code is contained in appname.py, and you start the app # using `python -m appname`, the main module package will report # as ''. Set the initial app name as None. # If the code is contained in appname.py, and you start the app # using `python appname.py`, the main module will report as None. # If the code is contained in a folder, and you start the app # using `python -m appname`, the main module will report as the # name of the folder. main_module_pkg = sys.modules['__main__'].__package__ if main_module_pkg == '': self._app_name = None else: self._app_name = main_module_pkg # During tests, and when running from a prompt, there won't be # a __main__ module. # Try deconstructing the app name from the app ID if self._app_name is None and app_id: self._app_name = app_id.split('.')[-1] # Load the app metdata (if it is available) # Apps packaged with Briefcase will have this metadata. try: self.metadata = importlib_metadata.metadata(self.module_name) except importlib_metadata.PackageNotFoundError: self.metadata = Message() # Now that we have metadata, we can fix the app name (in the case # where the app name and the module name differ - e.g., an app name # of ``hello-world`` will have a module name of ``hello_world``). # We use the PEP566-compliant key ``Name```, rather than the internally # consistent key ``App-Name```. if self.metadata['Name'] is not None: self._app_name = self.metadata['Name'] # Whatever app name has been given, speculatively attempt to import # the app module. Single-file apps won't have an app folder; apps with # misleading or misconfigured app names haven't given us enough # metadata to determine the app folder. In those cases, fall back to # an app name that *will* exist (``toga```) try: sys.modules[self.module_name] except KeyError: # Well that didn't work... self._app_name = 'toga' # If a name has been provided, use it; otherwise, look to # the module metadata. However, a name *must* be provided. if formal_name: self._formal_name = formal_name else: self._formal_name = self.metadata['Formal-Name'] if self._formal_name is None: raise RuntimeError('Toga application must have a formal name') # If an app_id has been provided, use it; otherwise, look to # the module metadata. However, an app_id *must* be provied if app_id: self._app_id = app_id else: self._app_id = self.metadata['App-ID'] if self._app_id is None: raise RuntimeError('Toga application must have an App ID') # If an author has been provided, use it; otherwise, look to # the module metadata. if author: self._author = author elif self.metadata['Author']: self._author = self.metadata['Author'] # If a version has been provided, use it; otherwise, look to # the module metadata. if version: self._version = version elif self.metadata['Version']: self._version = self.metadata['Version'] # If a home_page has been provided, use it; otherwise, look to # the module metadata. if home_page: self._home_page = home_page elif self.metadata['Home-page']: self._home_page = self.metadata['home_page'] # If a description has been provided, use it; otherwise, look to # the module metadata. if description: self._description = description elif self.metadata['description']: self._description = self.metadata['Summary'] # Set the application DOM ID; create an ID if one hasn't been provided. self._id = id if id else identifier(self) # Get a platform factory, and a paths instance from the factory. self.factory = get_platform_factory(factory) self.paths = self.factory.paths # If an icon (or icon name) has been explicitly provided, use it; # otherwise, the icon will be based on the app name. if icon: self.icon = icon else: self.icon = 'resources/{app_name}'.format(app_name=self.app_name) self.commands = CommandSet(factory=self.factory) self._startup_method = startup self._main_window = None self._on_exit = None self._full_screen_windows = None self._impl = self._create_impl() self.on_exit = on_exit
def decorate(message, hide_sender, sender): """ Notify the recipient about the sender rewrite. """ footer = [ '------------', 'Du kan ikke svare direkte på denne eposten.' ] if not hide_sender: footer.append( f'Opprinnelig avsender er {sender}, send svar til denne adressen.' ) footer.append( 'Denne eposten har uorginal avsender for å redusere risikoen for at ' 'meldingen oppfattes som spam.') else: footer.append( 'Opprinnelig avsender har valgt å skjule sin adresse.') footer = '\n'.join(footer) charset = message.get_content_charset() or 'us-ascii' content_type = message.get_content_type() wrap = True if not message.is_multipart() and content_type == 'text/plain': format_param = message.get_param('format') delsp = message.get_param('delsp') transfer_encoding = message.get('content-transfer-encoding') try: old_payload = message.get_payload(decode=True).decode(charset) del message['content-transfer-encoding'] footer_separator = '\n' payload = old_payload + footer_separator + footer for cset in (charset, 'utf-8'): try: message.set_payload(payload.encode(cset), cset) except UnicodeError: pass else: if format_param: message.set_param('format', format_param) if delsp: message.set_param('delsp', delsp) wrap = False break except (LookupError, UnicodeError): if transfer_encoding: del message['content-transfer-encoding'] message['Content-Transfer-Encoding'] = transfer_encoding elif message.get_content_type() == 'multipart/mixed': payload = message.get_payload() if not isinstance(payload, list): payload = [payload] mime_footer = MIMEText(footer.encode('utf-8'), 'plain', 'utf-8') mime_footer['Content-Disposition'] = 'inline' payload.append(mime_footer) message.set_payload(payload) wrap = False if not wrap: return inner = Message() for h, v in message.items(): if h.lower().startswith('content-'): inner[h] = v inner.set_payload(message.get_payload()) inner.set_unixfrom(message.get_unixfrom()) inner.preamble = message.preamble inner.epilogue = message.epilogue inner.set_default_type(message.get_default_type()) if hasattr(message, '__version__'): inner.__version__ = message.__version__ payload = [inner] mime_footer = MIMEText(footer.encode('utf-8'), 'plain', 'utf-8') mime_footer['Content-Disposition'] = 'inline' payload.append(mime_footer) message.set_payload(payload) del message['content-type'] del message['content-transfer-encoding'] del message['content-disposition'] message['Content-Type'] = 'multipart/mixed'
def header_check (db, cl, nodeid, new_values) : """ Check header of new messages and determine original customer from that header -- only if sender is the support special account (any account with system status). If send_to_customer flag is set *and* account is not a system account, munge the headers and add X-ROUNDUP-TO and X-ROUNDUP-CC headers. """ send_to_customer = False # Be sure to alway set send_to_customer to False! if 'send_to_customer' in new_values : send_to_customer = new_values ['send_to_customer'] new_values ['send_to_customer'] = False newmsgs = new_values.get ('messages') if not newmsgs : return newmsgs = set (newmsgs) if nodeid : oldmsgs = set (cl.get (nodeid, 'messages')) else : oldmsgs = set () system = db.user_status.lookup ('system') cemail = db.contact_type.lookup ('Email') for msgid in newmsgs.difference (oldmsgs) : msg = db.msg.getnode (msgid) h = None if msg.header : h = Parser ().parsestr (msg.header, headersonly = True) else : h = Message () if db.user.get (msg.author, 'status') == system : frm = fix_emails (h.get_all ('From')) subj = header_utf8 (h.get_all ('Subject') [0]) if ( frm and 'customer' not in new_values and 'emails' not in new_values ) : cc = {} if not nodeid : # use only first 'From' address (there shouldn't be more) rn, mail = getaddresses (frm) [0] # the *to* address in this mail is the support user we # want as a from-address for future mails *to* this user autad = None hto = fix_emails (h.get_all ('To')) if hto : torn, autad = getaddresses (hto) [0] if not autad.startswith ('support') : autad = None c = find_or_create_contact \ (db, mail, rn, frm = autad, subject = subj) cust = new_values ['customer'] = \ db.contact.get (c, 'customer') new_values ['emails'] = [c] else : supi = cl.getnode (nodeid) cust = supi.customer new_values ['emails'] = supi.emails new_values ['cc_emails'] = supi.cc_emails if supi.cc : cc = dict.fromkeys \ (x.strip ().lower () for x in supi.cc.split (',')) # Parse To and CC headers to find more customer email # addresses. Check if these contain the same domain # part as the From. ccs = h.get_all ('CC') or [] tos = h.get_all ('To') or [] if nodeid : tos.extend (frm) cfrm = db.customer.get (cust, 'fromaddress') alltocc = dict.fromkeys (new_values ['emails']) if 'cc_emails' in new_values : alltocc.update (dict.fromkeys (new_values ['cc_emails'])) for addrs, field in ((tos, 'emails'), (ccs, 'cc_emails')) : addrs = fix_emails (addrs) for rn, mail in getaddresses (addrs) : if mail == cfrm : continue c = find_or_create_contact \ (db, mail, rn, customer = cust) if c : if field not in new_values : new_values [field] = [] if c not in alltocc : new_values [field].append (c) alltocc [c] = 1 elif uidFromAddress (db, (rn, mail), create = 0) : # ignore internal addresses pass else : cc [mail.lower ()] = 1 if cc : new_values ['cc'] = ', '.join (cc.keys ()) else : if send_to_customer : mails = [] cc = [] if 'emails' in new_values : mails = new_values ['emails'] elif nodeid : mails = cl.get (nodeid, 'emails') if 'cc_emails' in new_values : mcc = new_values ['cc_emails'] elif nodeid : mcc = cl.get (nodeid, 'cc_emails') mails = mails or [] mcc = mcc or [] mails = (db.contact.get (x, 'contact') for x in mails) mcc = (db.contact.get (x, 'contact') for x in mcc) if 'cc' in new_values : cc = new_values ['cc'] elif nodeid : cc = cl.get (nodeid, 'cc') m = ','.join (mails) mc = ','.join (mcc) if mc : if cc : mc = ','.join ((mc, cc)) else : mc = cc if not m and not mc : raise Reject, \ _ ("Trying to send to customer with empty CC and " "without configured contact-email for customer" ) if m : h.add_header ('X-ROUNDUP-TO', m) if mc : h.add_header ('X-ROUNDUP-CC', mc) if 'bcc' in new_values : bcc = new_values ['bcc'] elif nodeid : bcc = cl.get (nodeid, 'bcc') if bcc : h.add_header ('X-ROUNDUP-BCC', bcc) # Time-Stamp of first reply to customer if not nodeid or not cl.get (nodeid, 'first_reply') : new_values ['first_reply'] = Date ('.') h = h.as_string () if h != '\n' and h != msg.header : db.msg.set (msgid, header = h)
def send_raw(self, job, message, config): config = dict(self.middleware.call_sync('mail.config'), **config) if config['fromname']: from_addr = Header(config['fromname'], 'utf-8') try: config['fromemail'].encode('ascii') except UnicodeEncodeError: from_addr.append(f'<{config["fromemail"]}>', 'utf-8') else: from_addr.append(f'<{config["fromemail"]}>', 'ascii') else: try: config['fromemail'].encode('ascii') except UnicodeEncodeError: from_addr = Header(config['fromemail'], 'utf-8') else: from_addr = Header(config['fromemail'], 'ascii') interval = message.get('interval') if interval is None: interval = timedelta() else: interval = timedelta(seconds=interval) sw_name = self.middleware.call_sync('system.info')['version'].split( '-', 1)[0] channel = message.get('channel') if not channel: channel = sw_name.lower() if interval > timedelta(): channelfile = '/tmp/.msg.%s' % (channel) last_update = datetime.now() - interval try: last_update = datetime.fromtimestamp( os.stat(channelfile).st_mtime) except OSError: pass timediff = datetime.now() - last_update if (timediff >= interval) or (timediff < timedelta()): # Make sure mtime is modified # We could use os.utime but this is simpler! with open(channelfile, 'w') as f: f.write('!') else: raise CallError( 'This message was already sent in the given interval') verrors = self.__password_verify(config['pass'], 'mail-config.pass') if verrors: raise verrors to = message.get('to') if not to: to = [ self.middleware.call_sync('user.query', [('username', '=', 'root')], {'get': True})['email'] ] if not to[0]: raise CallError('Email address for root is not configured') if message.get('attachments'): job.check_pipe("input") def read_json(): f = job.pipes.input.r data = b'' i = 0 while True: read = f.read(1048576) # 1MiB if read == b'': break data += read i += 1 if i > 50: raise ValueError( 'Attachments bigger than 50MB not allowed yet') if data == b'': return None return json.loads(data) attachments = read_json() else: attachments = None if 'html' in message or attachments: msg = MIMEMultipart() msg.preamble = 'This is a multi-part message in MIME format.' if 'html' in message: msg2 = MIMEMultipart('alternative') msg2.attach( MIMEText(message['text'], 'plain', _charset='utf-8')) msg2.attach(MIMEText(message['html'], 'html', _charset='utf-8')) msg.attach(msg2) if attachments: for attachment in attachments: m = Message() m.set_payload(attachment['content']) for header in attachment.get('headers'): m.add_header(header['name'], header['value'], **(header.get('params') or {})) msg.attach(m) else: msg = MIMEText(message['text'], _charset='utf-8') msg['Subject'] = message['subject'] msg['From'] = from_addr msg['To'] = ', '.join(to) if message.get('cc'): msg['Cc'] = ', '.join(message.get('cc')) msg['Date'] = formatdate() local_hostname = socket.gethostname() msg['Message-ID'] = "<%s-%s.%s@%s>" % ( sw_name.lower(), datetime.utcnow().strftime("%Y%m%d.%H%M%S.%f"), base64.urlsafe_b64encode(os.urandom(3)), local_hostname) extra_headers = message.get('extra_headers') or {} for key, val in list(extra_headers.items()): # We already have "Content-Type: multipart/mixed" and setting "Content-Type: text/plain" like some scripts # do will break python e-mail module. if key.lower() == "content-type": continue if key in msg: msg.replace_header(key, val) else: msg[key] = val syslog.openlog(logoption=syslog.LOG_PID, facility=syslog.LOG_MAIL) try: server = self._get_smtp_server(config, message['timeout'], local_hostname=local_hostname) # NOTE: Don't do this. # # If smtplib.SMTP* tells you to run connect() first, it's because the # mailserver it tried connecting to via the outgoing server argument # was unreachable and it tried to connect to 'localhost' and barfed. # This is because FreeNAS doesn't run a full MTA. # else: # server.connect() headers = '\n'.join([f'{k}: {v}' for k, v in msg._headers]) syslog.syslog(f"sending mail to {', '.join(to)}\n{headers}") server.sendmail(from_addr.encode(), to, msg.as_string()) server.quit() except Exception as e: # Don't spam syslog with these messages. They should only end up in the # test-email pane. # We are only interested in ValueError, not subclasses. if e.__class__ is ValueError: raise CallError(str(e)) syslog.syslog(f'Failed to send email to {", ".join(to)}: {str(e)}') if isinstance(e, smtplib.SMTPAuthenticationError): raise CallError( f'Authentication error ({e.smtp_code}): {e.smtp_error}', errno.EAUTH if osc.IS_FREEBSD else errno.EPERM) self.logger.warn('Failed to send email: %s', str(e), exc_info=True) if message['queue']: with MailQueue() as mq: mq.append(msg) raise CallError(f'Failed to send email: {e}') return True
def setUp(self): self.message = EmailMessage(recipient='recipient', sender='sender', message=Message())
def do_POST(self): try: # Connect to destination self._connect_to_host() except Exception as e: self.send_error(500, str(e)) return # The request that contains the request to the website is located # inside the POST request body from the stegoclient log.debug("Got stego-request from stegoclient") # io stream that represents the stego medium (i.e. image) req_body = io.BytesIO( self.rfile.read(int(self.headers.get("Content-Length", 0)))) stego_message = stego.extract(medium=req_body) # Get Host and Port from the original request host, port = self._get_hostaddr_from_headers(stego_message) # establish connection to the website log.info(f"Connecting to {host}:{port}") self.server = Server(host=host, port=port) # Just relay the original request to the website log.debug("Relaying extracted request to website") self.server.send(stego_message) # Parse response from website h = HTTPResponse(self.server.conn) h.begin() # Get rid of hop-by-hop headers self.filter_headers(h.msg) # Build response from website log.debug("Building response from website") stego_resp = self._build_stego_response(self.request_version, h.status, h.reason, h.msg, h.read()) # Build header to stegoclient header = Message() header.add_header("Host", f"{cfg.REMOTE_ADDR[0]}:{cfg.REMOTE_ADDR[1]}") header.add_header("Connection", "keep-alive") cover = self._get_cover_object() max_size = self._calc_max_size(cover) resp_len = len(stego_resp) if (max_size is not None and resp_len > max_size): log.debug( f"Can't fit response ({resp_len} bytes) into stego-response - " f"splitting into chunks of {max_size} bytes.") header.add_header("Transfer-Encoding", "chunked") resp_to_client = self._build_response_header( cfg.STEGO_HTTP_VERSION, h.status, h.reason, header) # Send headers to client log.debug("Sending chunked stego-response header to stegoclient") self.client.send(resp_to_client) start = time.time() chunk_count = 0 for chunk in self._split_into_chunks(stego_resp, max_size): if isinstance(cover, Image.Image): tmp_cover = cover.copy() else: tmp_cover = cover # Send chunks log.debug(f"Sending chunk with size: {len(chunk)} bytes") self._write_chunks(stego.embed(cover=tmp_cover, message=chunk)) chunk_count += 1 end = time.time() # send "end of chunks" trailer self._write_end_of_chunks() log.debug(f"{chunk_count} chunks sent in {end - start:.2f}s.") else: # Encapsulate response inside response to stego client log.debug("Embedding response from website in stego-response") start = time.time() stego_medium = stego.embed(cover=cover, message=stego_resp) end = time.time() log.debug( f"Took {end - start:.2f}s to embed response in stego-response") header.add_header("Content-Length", str(len(stego_medium))) resp_to_client = self._build_response(cfg.STEGO_HTTP_VERSION, h.status, h.reason, header, stego_medium) # Relay the message log.debug("Relaying stego-response to stegoclient") self.client.send(resp_to_client) if isinstance(cover, Image.Image): cover.close() # Let's close off the remote end h.close() self.server.close()
def run_and_output_changes(env, pool, send_via_email): import json from difflib import SequenceMatcher if not send_via_email: out = ConsoleOutput() else: import io out = FileOutput(io.StringIO(""), 70) # Run status checks. cur = BufferedOutput() run_checks(True, env, cur, pool) # Load previously saved status checks. cache_fn = "/var/cache/mailinabox/status_checks.json" if os.path.exists(cache_fn): prev = json.load(open(cache_fn)) # Group the serial output into categories by the headings. def group_by_heading(lines): from collections import OrderedDict ret = OrderedDict() k = [] ret["No Category"] = k for line_type, line_args, line_kwargs in lines: if line_type == "add_heading": k = [] ret[line_args[0]] = k else: k.append((line_type, line_args, line_kwargs)) return ret prev_status = group_by_heading(prev) cur_status = group_by_heading(cur.buf) # Compare the previous to the current status checks # category by category. for category, cur_lines in cur_status.items(): if category not in prev_status: out.add_heading(category + " -- Added") BufferedOutput(with_lines=cur_lines).playback(out) else: # Actual comparison starts here... prev_lines = prev_status[category] def stringify(lines): return [json.dumps(line) for line in lines] diff = SequenceMatcher(None, stringify(prev_lines), stringify(cur_lines)).get_opcodes() for op, i1, i2, j1, j2 in diff: if op == "replace": out.add_heading(category + " -- Previously:") elif op == "delete": out.add_heading(category + " -- Removed") if op in ("replace", "delete"): BufferedOutput( with_lines=prev_lines[i1:i2]).playback(out) if op == "replace": out.add_heading(category + " -- Currently:") elif op == "insert": out.add_heading(category + " -- Added") if op in ("replace", "insert"): BufferedOutput( with_lines=cur_lines[j1:j2]).playback(out) for category, prev_lines in prev_status.items(): if category not in cur_status: out.add_heading(category) out.print_warning("This section was removed.") if send_via_email: # If there were changes, send off an email. buf = out.buf.getvalue() if len(buf) > 0: # create MIME message from email.message import Message msg = Message() msg['From'] = "\"%s\" <administrator@%s>" % ( env['PRIMARY_HOSTNAME'], env['PRIMARY_HOSTNAME']) msg['To'] = "administrator@%s" % env['PRIMARY_HOSTNAME'] msg['Subject'] = "[%s] Status Checks Change Notice" % env[ 'PRIMARY_HOSTNAME'] msg.set_payload(buf, "UTF-8") # send to administrator@ import smtplib mailserver = smtplib.SMTP('localhost', 25) mailserver.ehlo() mailserver.sendmail( "administrator@%s" % env['PRIMARY_HOSTNAME'], # MAIL FROM "administrator@%s" % env['PRIMARY_HOSTNAME'], # RCPT TO msg.as_string()) mailserver.quit() # Store the current status checks output for next time. os.makedirs(os.path.dirname(cache_fn), exist_ok=True) with open(cache_fn, "w") as f: json.dump(cur.buf, f, indent=True)
def new_message(self): from email.message import Message return Message()
class MailParser(MailTool): """ methods for parsing message text, attachments subtle thing: Message object payloads are either a simple string for non-multipart messages, or a list of Message objects if multipart (possibly nested); we don't need to distinguish between the two cases here, because the Message walk generator always returns self first, and so works fine on non-multipart messages too (a single object is walked); for simple messages, the message body is always considered here to be the sole part of the mail; for multipart messages, the parts list includes the main message text, as well as all attachments; this allows simple messages not of type text to be handled like attachments in a UI (e.g., saved, opened); Message payload may also be None for some oddball part types; 4E note: in Py 3.1, text part payloads are returned as bytes for decode=1, and might be str otherwise; in mailtools, text is stored as bytes for file saves, but main-text bytes payloads are decoded to Unicode str per mail header info or platform default+guess; clients may need to convert other payloads: PyMailGUI uses headers to decode parts saved to binary files; 4E supports fetched message header auto-decoding per its own content, both for general headers such as Subject, as well as for names in address header such as From and To; client must request this after parse, before display: parser doesn't decode; """ def walkNamedParts(self, message): """ generator to avoid repeating part naming logic; skips multipart headers, makes part filenames; message is already parsed email.message.Message object; doesn't skip oddball types: payload may be None, must handle in part saves; some others may warrant skips too; """ for (ix, part) in enumerate(message.walk()): # walk includes message fulltype = part.get_content_type() # ix includes parts skipped maintype = part.get_content_maintype() if maintype == 'multipart': # multipart/*: container continue elif fulltype == 'message/rfc822': # 4E: skip message/rfc822 continue # skip all message/* too? else: filename, contype = self.partName(part, ix) yield (filename, contype, part) def partName(self, part, ix): """ extract filename and content type from message part; filename: tries Content-Disposition, then Content-Type name param, or generates one based on mimetype guess; """ filename = part.get_filename() # filename in msg hdrs? contype = part.get_content_type() # lowercase maintype/subtype if not filename: filename = part.get_param('name') # try content-type name if not filename: if contype == 'text/plain': # hardcode plain text ext ext = '.txt' # else guesses .ksh! else: ext = mimetypes.guess_extension(contype) if not ext: ext = '.bin' # use a generic default filename = 'part-%03d%s' % (ix, ext) return (self.decodeHeader(filename), contype) # oct 2011: decode i18n fnames def saveParts(self, savedir, message): """ store all parts of a message as files in a local directory; returns [('maintype/subtype', 'filename')] list for use by callers, but does not open any parts or attachments here; get_payload decodes base64, quoted-printable, uuencoded data; mail parser may give us a None payload for oddball types we probably should skip over: convert to str here to be safe; """ if not os.path.exists(savedir): os.mkdir(savedir) partfiles = [] for (filename, contype, part) in self.walkNamedParts(message): fullname = os.path.join(savedir, filename) fileobj = open(fullname, 'wb') # use binary mode content = part.get_payload(decode=1) # decode base64,qp,uu if not isinstance(content, bytes): # 4E: need bytes for rb content = b'(no content)' # decode=1 returns bytes, fileobj.write(content) # but some payloads None fileobj.close() # 4E: not str(content) partfiles.append((contype, fullname)) # for caller to open return partfiles def saveOnePart(self, savedir, partname, message): """ ditto, but find and save just one part by name """ if not os.path.exists(savedir): os.mkdir(savedir) fullname = os.path.join(savedir, partname) (contype, content) = self.findOnePart(partname, message) if not isinstance(content, bytes): # 4E: need bytes for rb content = b'(no content)' # decode=1 returns bytes, open(fullname, 'wb').write(content) # but some payloads None return (contype, fullname) # 4E: not str(content) def partsList(self, message): """" return a list of filenames for all parts of an already parsed message, using same filename logic as saveParts, but do not store the part files here """ validParts = self.walkNamedParts(message) return [filename for (filename, contype, part) in validParts] def findOnePart(self, partname, message): """ find and return part's content, given its name; intended to be used in conjunction with partsList; we could also mimetypes.guess_type(partname) here; we could also avoid this search by saving in dict; 4E: content may be str or bytes--convert as needed; """ for (filename, contype, part) in self.walkNamedParts(message): if filename == partname: content = part.get_payload(decode=1) # does base64,qp,uu return (contype, content) # may be bytes text def decodedPayload(self, part, asStr=True): """ 4E: decode text part bytes to Unicode str for display, line wrap, etc.; part is a Message; (decode=1) undoes MIME email encodings (base64, uuencode, qp), bytes.decode() performs additional Unicode text string decodings; tries charset encoding name in message headers first (if present, and accurate), then tries platform defaults and a few guesses before giving up with error string; """ payload = part.get_payload(decode=1) # payload may be bytes if asStr and isinstance(payload, bytes): # decode=1 returns bytes tries = [] enchdr = part.get_content_charset() # try msg headers first! if enchdr: tries += [enchdr] # try headers first tries += [sys.getdefaultencoding()] # same as bytes.decode() tries += ['latin1', 'utf8'] # try 8-bit, incl ascii for trie in tries: # try utf8 (windows dflt) try: payload = payload.decode(trie) # give it a shot, eh? break except (UnicodeError, LookupError): # lookuperr: bad name pass else: payload = '--Sorry: cannot decode Unicode text--' return payload def findMainText(self, message, asStr=True): """ for text-oriented clients, return first text part's str; for the payload of a simple message, or all parts of a multipart message, looks for text/plain, then text/html, then text/*, before deducing that there is no text to display; this is a heuristic, but covers most simple, multipart/alternative, and multipart/mixed messages; content-type defaults to text/plain if not in simple msg; handles message nesting at top level by walking instead of list scans; if non-multipart but type is text/html, returns the HTML as the text with an HTML type: caller may open in web browser, extract plain text, etc; if nonmultipart and not text, there is no text to display: save/open message content in UI; caveat: does not try to concatenate multiple inline text/plain parts if any; 4E: text payloads may be bytes--decodes to str here; 4E: asStr=False to get raw bytes for HTML file saves; """ # try to find a plain text for part in message.walk(): # walk visits message type = part.get_content_type() # if nonmultipart if type == 'text/plain': # may be base64,qp,uu return type, self.decodedPayload(part, asStr) # bytes to str too? # try to find an HTML part for part in message.walk(): type = part.get_content_type() # caller renders html if type == 'text/html': return type, self.decodedPayload(part, asStr) # try any other text type, including XML for part in message.walk(): if part.get_content_maintype() == 'text': return part.get_content_type(), self.decodedPayload(part, asStr) # punt: could use first part, but it's not marked as text failtext = '[No text to display]' if asStr else b'[No text to display]' return 'text/plain', failtext def decodeHeader(self, rawheader): """ 4E: decode existing i18n message header text per both email and Unicode standards, according to its content; return as is if unencoded or fails; client must call this to display: parsed Message object does not decode; i18n header example: '=?UTF-8?Q?Introducing=20Top=20Values=20..Savers?='; i18n header example: 'Man where did you get that =?UTF-8?Q?assistant=3F?='; decode_header handles any line breaks in header string automatically, may return multiple parts if any substrings of hdr are encoded, and returns all bytes in parts list if any encodings found (with unencoded parts encoded as raw-unicode-escape and enc=None) but returns a single part with enc=None that is str instead of bytes in Py3.1 if the entire header is unencoded (must handle mixed types here); see Chapter 13 for more details/examples; the following first attempt code was okay unless any encoded substrings, or enc was returned as None (raised except which returned rawheader unchanged): hdr, enc = email.header.decode_header(rawheader)[0] return hdr.decode(enc) # fails if enc=None: no encoding or encoded substrs """ try: parts = email.header.decode_header(rawheader) decoded = [] for (part, enc) in parts: # for all substrings if enc == None: # part unencoded? if not isinstance(part, bytes): # str: full hdr unencoded decoded += [part] # else do unicode decode else: decoded += [part.decode('raw-unicode-escape')] else: decoded += [part.decode(enc)] return ' '.join(decoded) except: return rawheader # punt! def decodeAddrHeader(self, rawheader): """ 4E: decode existing i18n address header text per email and Unicode, according to its content; must parse out first part of email address to get i18n part: '"=?UTF-8?Q?Walmart?=" <*****@*****.**>'; From will probably have just 1 addr, but To, Cc, Bcc may have many; decodeHeader handles nested encoded substrings within an entire hdr, but we can't simply call it for entire hdr here because it fails if encoded name substring ends in " quote instead of whitespace or endstr; see also encodeAddrHeader in mailSender module for the inverse of this; the following first attempt code failed to handle encoded substrings in name, and raised exc for unencoded bytes parts if any encoded substrings; namebytes, nameenc = email.header.decode_header(name)[0] (do email+MIME) if nameenc: name = namebytes.decode(nameenc) (do Unicode?) """ try: pairs = email.utils.getaddresses([rawheader]) # split addrs and parts decoded = [] # handles name commas for (name, addr) in pairs: try: name = self.decodeHeader(name) # email+MIME+Uni except: name = None # but uses encooded name if exc in decodeHeader joined = email.utils.formataddr((name, addr)) # join parts decoded.append(joined) return ', '.join(decoded) # >= 1 addrs except: return self.decodeHeader(rawheader) # try decoding entire string def splitAddresses(self, field): """ 4E: use comma separator for multiple addrs in the UI, and getaddresses to split correctly and allow for comma in the name parts of addresses; used by PyMailGUI to split To, Cc, Bcc as needed for user inputs and copied headers; returns empty list if field is empty, or any exception occurs; """ try: pairs = email.utils.getaddresses([field]) # [(name,addr)] return [email.utils.formataddr(pair) for pair in pairs] # [name <addr>] except: return '' # syntax error in user-entered field?, etc. # returned when parses fail errorMessage = Message() errorMessage.set_payload('[Unable to parse message - format error]') def parseHeaders(self, mailtext): """ parse headers only, return root email.message.Message object stops after headers parsed, even if nothing else follows (top) email.message.Message object is a mapping for mail header fields payload of message object is None, not raw body text """ try: return email.parser.Parser().parsestr(mailtext, headersonly=True) except: return self.errorMessage def parseMessage(self, fulltext): """ parse entire message, return root email.message.Message object payload of message object is a string if not is_multipart() payload of message object is more Messages if multiple parts the call here same as calling email.message_from_string() """ try: return email.parser.Parser().parsestr(fulltext) # may fail! except: return self.errorMessage # or let call handle? can check return def parseMessageRaw(self, fulltext): """ parse headers only, return root email.message.Message object stops after headers parsed, for efficiency (not yet used here) payload of message object is raw text of mail after headers """ try: return email.parser.HeaderParser().parsestr(fulltext) except: return self.errorMessage
def test_readfile_operations(self): class ITest(Interface): title = schema.TextLine() body = schema.Text() alsoProvides(ITest['body'], IPrimaryField) fti_mock = DexterityFTI(u'testtype') fti_mock.lookupSchema = Mock(return_value=ITest) fti_mock.behaviors = [ITestBehavior.__identifier__] self.mock_utility(fti_mock, IDexterityFTI, name=u"testtype") item = Item('item') item.portal_type = 'testtype' readfile = DefaultReadFile(item) message = Message() message['title'] = 'Test title' message['foo'] = '10' message['bar'] = 'xyz' message.set_payload('<p>body</p>') from plone.rfc822 import constructMessageFromSchemata self.patch_global(constructMessageFromSchemata, return_value=message) body = b"""\ title: Test title foo: 10 bar: xyz Portal-Type: testtype <p>body</p>""" # iter # next self.assertEqual(body, readfile.read()) self.assertEqual(69, readfile.size()) self.assertEqual('utf-8', readfile.encoding) self.assertEqual(None, readfile.name) self.assertEqual('text/plain', readfile.mimeType) readfile.seek(2) self.assertEqual(2, readfile.tell()) self.assertEqual(b'tl', readfile.read(2)) self.assertEqual(4, readfile.tell()) readfile.seek(0, 2) self.assertEqual(69, readfile.tell()) readfile.seek(0) self.assertEqual(b'foo: 10\n', readfile.readlines()[1]) readfile.seek(0) self.assertEqual(b'foo: 10\n', readfile.readlines(100)[1]) readfile.seek(0) self.assertEqual(b'title: Test title\n', readfile.readline()) readfile.seek(0) self.assertEqual(b'title: Test title\n', readfile.readline(100)) readfile.seek(0) self.assertEqual(b'foo: 10\n', list(iter(readfile))[1]) self.assertEqual(False, readfile.closed) readfile.close()
def test_bytes_generator(): """Test the email bytes generator class.""" message = Message() message.set_type('application/pkcs7-mime') assert utils.mime_to_bytes(message) == b'MIME-Version: 1.0\r\n' \ b'Content-Type: application/pkcs7-mime\r\n\r\n'
def createEmail(self, msgdict, builderName, title, results, builds=None, patches=None, logs=None): text = msgdict['body'].encode(ENCODING) type = msgdict['type'] if 'subject' in msgdict: subject = msgdict['subject'].encode(ENCODING) else: subject = self.subject % { 'result': Results[results], 'projectName': title, 'title': title, 'builder': builderName, } assert '\n' not in subject, \ "Subject cannot contain newlines" assert type in ('plain', 'html'), \ "'%s' message type must be 'plain' or 'html'." % type if patches or logs: m = MIMEMultipart() m.attach(MIMEText(text, type, ENCODING)) else: m = Message() m.set_payload(text, ENCODING) m.set_type("text/%s" % type) m['Date'] = formatdate(localtime=True) m['Subject'] = subject m['From'] = self.fromaddr # m['To'] is added later if patches: for (i, patch) in enumerate(patches): a = self.patch_to_attachment(patch, i) m.attach(a) if logs: for log in logs: name = "%s.%s" % (log.getStep().getName(), log.getName()) if (self._shouldAttachLog(log.getName()) or self._shouldAttachLog(name)): # Use distinct filenames for the e-mail summary if self.buildSetSummary: filename = "%s.%s.%s" % ( log.getStep().getBuild().getBuilder().getName(), log.getStep().getName(), log.getName()) else: filename = name text = log.getText() if not isinstance(text, unicode): # guess at the encoding, and use replacement symbols # for anything that's not in that encoding text = text.decode(LOG_ENCODING, 'replace') a = MIMEText(text.encode(ENCODING), _charset=ENCODING) a.add_header('Content-Disposition', "attachment", filename=filename) m.attach(a) #@todo: is there a better way to do this? # Add any extra headers that were requested, doing WithProperties # interpolation if only one build was given if self.extraHeaders: if len(builds) == 1: d = builds[0].render(self.extraHeaders) else: d = defer.succeed(self.extraHeaders) @d.addCallback def addExtraHeaders(extraHeaders): for k, v in extraHeaders.items(): if k in m: twlog.msg("Warning: Got header " + k + " in self.extraHeaders " "but it already exists in the Message - " "not adding it.") m[k] = v d.addCallback(lambda _: m) return d return defer.succeed(m)
def parseform(self, limit = 67108864, tostr = True, safename = True): ''' Parse form-data with multipart/form-data or application/x-www-form-urlencoded In Python3, the keys of form and files are unicode, but values are bytes If the key ends with '[]', it is considered to be a list: a=1&b=2&b=3 => {'a':1,'b':3} a[]=1&b[]=2&b[]=3 => {'a':[1],'b':[2,3]} :param limit: limit total input size, default to 64MB. None = no limit. Note that all the form data is stored in memory (including upload files), so it is dangerous to accept a very large input. :param tostr: convert values to str in Python3. Only apply to form, files data are always bytes :param safename: if True, extra security checks are performed on filenames to reduce known security risks. ''' if tostr: def _str(s): try: if not isinstance(s, str): return s.decode(self.encoding) else: return s except: raise HttpInputException('Invalid encoding in post data: ' + repr(s)) else: def _str(s): return s try: form = {} files = {} # If there is not a content-type header, maybe there is not a content. if b'content-type' in self.headerdict and self.inputstream is not None: contenttype = self.headerdict[b'content-type'] m = Message() # Email library expects string, which is unicode in Python 3 try: m.add_header('Content-Type', str(contenttype.decode('ascii'))) except UnicodeDecodeError: raise HttpInputException('Content-Type has non-ascii characters') if m.get_content_type() == 'multipart/form-data': fp = BytesFeedParser() fp.feed(b'Content-Type: ' + contenttype + b'\r\n\r\n') total_length = 0 while True: try: for m in self.inputstream.prepareRead(self.container): yield m data = self.inputstream.readonce() total_length += len(data) if limit is not None and total_length > limit: raise HttpInputException('Data is too large') fp.feed(data) except EOFError: break msg = fp.close() if not msg.is_multipart() or msg.defects: # Reject the data raise HttpInputException('Not valid multipart/form-data format') for part in msg.get_payload(): if part.is_multipart() or part.defects: raise HttpInputException('Not valid multipart/form-data format') disposition = part.get_params(header='content-disposition') if not disposition: raise HttpInputException('Not valid multipart/form-data format') disposition = dict(disposition) if 'form-data' not in disposition or 'name' not in disposition: raise HttpInputException('Not valid multipart/form-data format') if 'filename' in disposition: name = disposition['name'] filename = disposition['filename'] if safename: filename = _safename(filename) if name.endswith('[]'): files.setdefault(name[:-2], []).append({'filename': filename, 'content': part.get_payload(decode=True)}) else: files[name] = {'filename': filename, 'content': part.get_payload(decode=True)} else: name = disposition['name'] if name.endswith('[]'): form.setdefault(name[:-2], []).append(_str(part.get_payload(decode=True))) else: form[name] = _str(part.get_payload(decode=True)) elif m.get_content_type() == 'application/x-www-form-urlencoded' or \ m.get_content_type() == 'application/x-url-encoded': if limit is not None: for m in self.inputstream.read(self.container, limit + 1): yield m data = self.container.data if len(data) > limit: raise HttpInputException('Data is too large') else: for m in self.inputstream.read(self.container): yield m data = self.container.data result = parse_qs(data, True) def convert(k,v): try: k = str(k.decode('ascii')) except: raise HttpInputException('Form-data key must be ASCII') if not k.endswith('[]'): v = _str(v[-1]) else: k = k[:-2] v = [_str(i) for i in v] return (k,v) form = dict(convert(k,v) for k,v in result.items()) else: # Other formats, treat like no data pass self.form = form self.files = files except Exception as exc: raise HttpInputException('Failed to parse form-data: ' + str(exc))
def prepare_multipart_body(self, content_index=0): # type: (int) -> int """Will prepare the body of this request according to the multipart information. This call assumes the on_request policies have been applied already in their correct context (sync/async) Does nothing if "set_multipart_mixed" was never called. :param int content_index: The current index of parts within the batch message. :returns: The updated index after all parts in this request have been added. :rtype: int """ if not self.multipart_mixed_info: return 0 requests = self.multipart_mixed_info[0] # type: List[HttpRequest] boundary = self.multipart_mixed_info[2] # type: Optional[str] # Update the main request with the body main_message = Message() main_message.add_header("Content-Type", "multipart/mixed") if boundary: main_message.set_boundary(boundary) for req in requests: part_message = Message() if req.multipart_mixed_info: content_index = req.prepare_multipart_body( content_index=content_index) part_message.add_header("Content-Type", req.headers['Content-Type']) payload = req.serialize() # We need to remove the ~HTTP/1.1 prefix along with the added content-length payload = payload[payload.index(b'--'):] else: part_message.add_header("Content-Type", "application/http") part_message.add_header("Content-Transfer-Encoding", "binary") part_message.add_header("Content-ID", str(content_index)) payload = req.serialize() content_index += 1 part_message.set_payload(payload) main_message.attach(part_message) try: from email.policy import HTTP full_message = main_message.as_bytes(policy=HTTP) eol = b"\r\n" except ImportError: # Python 2.7 # Right now we decide to not support Python 2.7 on serialization, since # it doesn't serialize a valid HTTP request (and our main scenario Storage refuses it) raise NotImplementedError( "Multipart request are not supported on Python 2.7") # full_message = main_message.as_string() # eol = b'\n' _, _, body = full_message.split(eol, 2) self.set_bytes_body(body) self.headers["Content-Type"] = ("multipart/mixed; boundary=" + main_message.get_boundary()) return content_index
def sendmail(subject, text, to=None, cc=None, bcc=None, mail_from=None, html=None): """ Create and send a text/plain message Return a tuple of success or error indicator and message. :param subject: subject of email :type subject: unicode :param text: email body text :type text: unicode :param to: recipients :type to: list :param cc: recipients (CC) :type cc: list :param bcc: recipients (BCC) :type bcc: list :param mail_from: override default mail_from :type mail_from: unicode :param html: html email body text :type html: unicode :rtype: tuple :returns: (is_ok, Description of error or OK message) """ import smtplib import socket from email.message import Message from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.charset import Charset, QP from email.utils import formatdate, make_msgid cfg = app.cfg if not cfg.mail_enabled: return (0, _("Contact administrator: cannot send password recovery e-mail " "because mail configuration is incomplete.")) mail_from = mail_from or cfg.mail_from logging.debug("send mail, from: {0!r}, subj: {1!r}".format(mail_from, subject)) logging.debug("send mail, to: {0!r}".format(to)) if not to and not cc and not bcc: return 1, _("No recipients, nothing to do") subject = subject.encode(CHARSET) # Create a text/plain body using CRLF (see RFC2822) text = text.replace(u'\n', u'\r\n') text = text.encode(CHARSET) # Create a message using CHARSET and quoted printable # encoding, which should be supported better by mail clients. # TODO: check if its really works better for major mail clients text_msg = Message() charset = Charset(CHARSET) charset.header_encoding = QP charset.body_encoding = QP text_msg.set_charset(charset) # work around a bug in python 2.4.3 and above: text_msg.set_payload('=') if text_msg.as_string().endswith('='): text = charset.body_encode(text) text_msg.set_payload(text) if html: msg = MIMEMultipart('alternative') msg.attach(text_msg) html = html.encode(CHARSET) html_msg = MIMEText(html, 'html') html_msg.set_charset(charset) msg.attach(html_msg) else: msg = text_msg address = encodeAddress(mail_from, charset) msg['From'] = address if to: msg['To'] = ','.join(to) if cc: msg['CC'] = ','.join(cc) msg['Date'] = formatdate() msg['Message-ID'] = make_msgid() msg['Subject'] = Header(subject, charset) # See RFC 3834 section 5: msg['Auto-Submitted'] = 'auto-generated' if cfg.mail_sendmail: if bcc: # Set the BCC. This will be stripped later by sendmail. msg['BCC'] = ','.join(bcc) # Set Return-Path so that it isn't set (generally incorrectly) for us. msg['Return-Path'] = address # Send the message if not cfg.mail_sendmail: try: logging.debug("trying to send mail (smtp) via smtp server '{0}'".format(cfg.mail_smarthost)) host, port = (cfg.mail_smarthost + ':25').split(':')[:2] server = smtplib.SMTP(host, int(port)) try: # server.set_debuglevel(1) if cfg.mail_username is not None and cfg.mail_password is not None: try: # try to do TLS server.ehlo() if server.has_extn('starttls'): server.starttls() server.ehlo() logging.debug("tls connection to smtp server established") except: logging.debug("could not establish a tls connection to smtp server, continuing without tls") logging.debug("trying to log in to smtp server using account '{0}'".format(cfg.mail_username)) server.login(cfg.mail_username, cfg.mail_password) server.sendmail(mail_from, (to or []) + (cc or []) + (bcc or []), msg.as_string()) finally: try: server.quit() except AttributeError: # in case the connection failed, SMTP has no "sock" attribute pass except smtplib.SMTPException as e: logging.exception("smtp mail failed with an exception.") return 0, str(e) except (os.error, socket.error) as e: logging.exception("smtp mail failed with an exception.") return (0, _("Connection to mailserver '%(server)s' failed: %(reason)s", server=cfg.mail_smarthost, reason=str(e) )) else: try: logging.debug("trying to send mail (sendmail)") sendmailp = os.popen(cfg.mail_sendmail, "w") # msg contains everything we need, so this is a simple write sendmailp.write(msg.as_string()) sendmail_status = sendmailp.close() if sendmail_status: logging.error("sendmail failed with status: {0!s}".format(sendmail_status)) return 0, str(sendmail_status) except: logging.exception("sendmail failed with an exception.") return 0, _("Mail not sent") logging.debug("Mail sent successfully") return 1, _("Mail sent successfully")
def createEmail(self, msgdict, builderName, title, results, builds=None, patches=None, logs=None): text = msgdict['body'] type = msgdict['type'] if msgdict.get('subject') is not None: subject = msgdict['subject'] else: subject = self.subject % {'result': Results[results], 'projectName': title, 'title': title, 'builder': builderName} assert '\n' not in subject, \ "Subject cannot contain newlines" assert type in ('plain', 'html'), \ "'%s' message type must be 'plain' or 'html'." % type if patches or logs: m = MIMEMultipart() txt = MIMEText(text, type, ENCODING) m.attach(txt) else: m = Message() m.set_payload(text, ENCODING) m.set_type("text/%s" % type) m['Date'] = formatdate(localtime=True) m['Subject'] = subject m['From'] = self.fromaddr # m['To'] is added later if patches: for (i, patch) in enumerate(patches): a = self.patch_to_attachment(patch, i) m.attach(a) if logs: for log in logs: name = "%s.%s" % (log['stepname'], log['name']) if (self._shouldAttachLog(log['name']) or self._shouldAttachLog(name)): # Use distinct filenames for the e-mail summary if self.buildSetSummary: filename = "%s.%s" % (log['buildername'], name) else: filename = name text = log['content']['content'] a = MIMEText(text.encode(ENCODING), _charset=ENCODING) a.add_header('Content-Disposition', "attachment", filename=filename) m.attach(a) # @todo: is there a better way to do this? # Add any extra headers that were requested, doing WithProperties # interpolation if only one build was given if self.extraHeaders: extraHeaders = self.extraHeaders if len(builds) == 1: props = Properties.fromDict(builds[0]['properties']) extraHeaders = yield props.render(extraHeaders) for k, v in iteritems(extraHeaders): if k in m: twlog.msg("Warning: Got header " + k + " in self.extraHeaders " "but it already exists in the Message - " "not adding it.") m[k] = v defer.returnValue(m)
def blank(): return Message()