def do_command(self, cmd, args=None): if args is None: args = () # Try to import a command handler module for this command modname = 'Mailman.Commands.cmd_' + cmd try: __import__(modname) handler = sys.modules[modname] # ValueError can be raised if cmd has dots in it. except (ImportError, ValueError): # If we're on line zero, it was the Subject: header that didn't # contain a command. It's possible there's a Re: prefix (or # localized version thereof) on the Subject: line that's messing # things up. Pop the prefix off and try again... once. # # If that still didn't work it isn't enough to stop processing. # BAW: should we include a message that the Subject: was ignored? if not self.subjcmdretried and args: self.subjcmdretried += 1 cmd = args.pop(0) return self.do_command(cmd, args) return self.lineno <> 0 # with Dlists, we don't allow email subscription if DlistUtils.enabled(self.mlist) and (cmd == 'subscribe' or cmd == 'join'): realname = self.mlist.real_name domain = Utils.get_domain() self.results.append(Utils.wrap(_("""\ This list cannot be subscribed to via email. Please use the website at http://%(domain)s/mailman/listinfo/%(realname)s . """))) return self.lineno <> 0 # superstitious behavior as they do it above return handler.process(self, args)
def changeMemberAddress(self, member, newaddress, nodelete=1): subscriber = DlistUtils.Subscriber(self.__mlist) oldm.changeMemberAddress(self, member, newaddress, nodelete=0) #changed nodelete to 0 - Anna memberkey = member.lower() if DlistUtils.enabled(self.__mlist): subscriber.changeAddress(memberkey, newaddress) database = self._dbconnect() store = Store(database) result = store.find(StormMembers, StormMembers.address == unicode(member,"utf-8") , StormMembers.listname == unicode(self.list,"utf-8")) result.set(address = unicode(newaddress,"utf-8")) store.commit()
def removeMember(self, member, affect_dlist_database=1): subscriber = DlistUtils.Subscriber(self.__mlist) oldm.removeMember(self, member) memberkey = member.lower() if affect_dlist_database and DlistUtils.enabled(self.__mlist): subscriber.unsubscribeFromList(memberkey) database = self._dbconnect() store = Store(database) result = store.find(StormMembers, StormMembers.address == unicode(member,"utf-8") , StormMembers.listname == unicode(self.list,"utf-8")) result.remove() store.commit()
def setMemberOption(self, member, flag, value): subscriber = DlistUtils.Subscriber(self.__mlist) oldm.setMemberOption(self, member, flag, value) if flag == mm_cfg.Digests and DlistUtils.enabled(self.__mlist): subscriber.setDigest(self.__mlist, member, value) missing = [] database = self._dbconnect() store = Store(database) result = store.find(StormMembers, StormMembers.address == unicode(member,"utf-8") , StormMembers.listname == unicode(self.list,"utf-8")) if flag == mm_cfg.Digests: if value: # Be sure the list supports digest delivery if not self.__mlist.digestable: raise Errors.CantDigestError # The user is turning on digest mode for members in result: if(members.digest == unicode("Y","utf-8")): raise Errors.AlreadyReceivingDigests, member cpuser = store.find(StormMembers, StormMembers.address == unicode(member,"utf-8") , StormMembers.listname == unicode(self.list,"utf-8"), StormMembers.digest == unicode("N","utf-8")) if cpuser is None: raise Errors.NotAMemberError, member result.set(digest = u"Y") store.commit() else: # Be sure the list supports regular delivery if not self.__mlist.nondigestable: raise Errors.MustDigestError # The user is turning off digest mode for members in result: if(members.digest == unicode("N","utf-8")): raise Errors.AlreadyReceivingRegularDeliveries, member cpuser = store.find(StormMembers, StormMembers.address == unicode(member,"utf-8") , StormMembers.listname == unicode(self.list,"utf-8"), StormMembers.digest == unicode("Y","utf-8")) if cpuser is None: raise Errors.NotAMemberError, member result.set(digest = u"N") store.commit() # When toggling off digest delivery, we want to be sure to set # things up so that the user receives one last digest, # otherwise they may lose some email #self.__mlist.one_last_digest[memberkey] = cpuser # We don't need to touch user_options because the digest state # isn't kept as a bitfield flag. return options = 0 if value: options = options|flag else: options = options & ~flag result.set(user_options = options) store.commit()
def process(mlist, msg, msgdata): """ Process a command for a dlist given in an email, such as create a new thread, subscribe to or unsubscribe from a thread""" if not DlistUtils.enabled(mlist): return thread = DlistUtils.Thread(mlist) # Ensure that there is a subject, even if it's the empty string if not msg.has_key('Subject'): msg['Subject'] = '' try: # To and CC could be anything, but we know x-original-to will be # the list address in question. incomingAddress = msg['X-Original-To'].split('@')[0] # strip domain commands = incomingAddress.split('+')[1:] # strip listname except Exception, e: raise ErrorsDlist.MalformedRequest(get_malformed_msg_txt(mlist))
def SendSubscribeAck(self, name, password, digest, text=''): pluser = self.getMemberLanguage(name) if self.welcome_msg: welcome = Utils.wrap(self.welcome_msg) + '\n' else: welcome = '' if self.umbrella_list: addr = self.GetMemberAdminEmail(name) umbrella = Utils.wrap(_('''\ Note: Since this is a list of mailing lists, administrative notices like the password reminder will be sent to your membership administrative address, %(addr)s.''')) else: umbrella = '' #added to support a different template for Dlists if DlistUtils.enabled(self): template = "subscribeack-dyn.txt" else: template = "subscribeack.txt" # get the text from the template text += Utils.maketext( template, {'real_name' : self.real_name, 'host_name' : self.host_name, 'welcome' : welcome, 'umbrella' : umbrella, 'emailaddr' : self.GetListEmail(), 'listinfo_url': self.GetScriptURL('listinfo', absolute=True), 'optionsurl' : self.GetOptionsURL(name, absolute=True), 'password' : password, 'user' : self.getMemberCPAddress(name), }, lang=pluser, mlist=self) if digest: digmode = _(' (Digest mode)') else: digmode = '' realname = self.real_name msg = Message.UserNotification( self.GetMemberAdminEmail(name), self.GetRequestEmail(), _('Welcome to the "%(realname)s" mailing list%(digmode)s'), text, pluser) msg['X-No-Archive'] = 'yes' msg.send(self, verp=mm_cfg.VERP_PERSONALIZED_DELIVERIES)
def setDeliveryStatus(self, member, status): oldm.setDeliveryStatus(self, member, status) subscriber = DlistUtils.Subscriber(self.__mlist) memberkey = member.lower() enabled = (status == MemberAdaptor.ENABLED) if DlistUtils.enabled(self.__mlist): subscriber.setDisable(member, (not enabled)) assert status in (MemberAdaptor.ENABLED, MemberAdaptor.UNKNOWN, MemberAdaptor.BYUSER, MemberAdaptor.BYADMIN, MemberAdaptor.BYBOUNCE) if status == MemberAdaptor.ENABLED: self.setBounceInfo(member,None) else: database = self._dbconnect() store = Store(database) time = datetime.datetime.now() result = store.find(StormMembers, StormMembers.address == unicode(member,"utf-8") , StormMembers.listname == unicode(self.list,"utf-8")) result.set(delivery_status = status) #result.set(delivery_status_timestamp = unicode(time.ctime(),"utf-8")) result.set(delivery_status_timestamp = time) store.commit()
def GetMailmanFooter(self): if DlistUtils.enabled(self): return "<footer>" ownertext = COMMASPACE.join([Utils.ObscureEmail(a, 1) for a in self.owner]) # Remove the .Format() when htmlformat conversion is done. realname = self.real_name hostname = self.host_name listinfo_link = Link(self.GetScriptURL('listinfo-dyn'), realname).Format() owner_link = Link('mailto:' + self.GetOwnerEmail(), ownertext).Format() innertext = _('%(listinfo_link)s list run by %(owner_link)s') return Container( '<hr>', Address( Container( innertext, '<br>', Link(self.GetScriptURL('admin'), _('%(realname)s administrative interface')), _(' (requires authorization)'), '<br>', Link(Utils.ScriptURL('listinfo'), _('Overview of all %(hostname)s mailing lists')), '<p>', MailmanLogo()))).Format()
def process(mlist, msg, msgdata): # Set the "X-Ack: no" header if noack flag is set. if msgdata.get('noack'): del msg['x-ack'] msg['X-Ack'] = 'no' # Because we're going to modify various important headers in the email # message, we want to save some of the information in the msgdata # dictionary for later. Specifically, the sender header will get waxed, # but we need it for the Acknowledge module later. msgdata['original_sender'] = msg.get_sender() # VirginRunner sets _fasttrack for internally crafted messages. fasttrack = msgdata.get('_fasttrack') if not msgdata.get('isdigest') and not fasttrack: try: prefix_subject(mlist, msg, msgdata) except (UnicodeError, ValueError): # TK: Sometimes subject header is not MIME encoded for 8bit # simply abort prefixing. pass # Mark message so we know we've been here, but leave any existing # X-BeenThere's intact. msg['X-BeenThere'] = mlist.GetListEmail() # Add Precedence: and other useful headers. None of these are standard # and finding information on some of them are fairly difficult. Some are # just common practice, and we'll add more here as they become necessary. # Good places to look are: # # http://www.dsv.su.se/~jpalme/ietf/jp-ietf-home.html # http://www.faqs.org/rfcs/rfc2076.html # # None of these headers are added if they already exist. BAW: some # consider the advertising of this a security breach. I.e. if there are # known exploits in a particular version of Mailman and we know a site is # using such an old version, they may be vulnerable. It's too easy to # edit the code to add a configuration variable to handle this. if not msg.has_key('x-mailman-version'): msg['X-Mailman-Version'] = mm_cfg.VERSION # We set "Precedence: list" because this is the recommendation from the # sendmail docs, the most authoritative source of this header's semantics. if not msg.has_key('precedence'): msg['Precedence'] = 'list' # Reply-To: munging. Do not do this if the message is "fast tracked", # meaning it is internally crafted and delivered to a specific user. BAW: # Yuck, I really hate this feature but I've caved under the sheer pressure # of the (very vocal) folks want it. OTOH, RFC 2822 allows Reply-To: to # be a list of addresses, so instead of replacing the original, simply # augment it. RFC 2822 allows max one Reply-To: header so collapse them # if we're adding a value, otherwise don't touch it. (Should we collapse # in all cases?) if not fasttrack: # A convenience function, requires nested scopes. pair is (name, addr) new = [] d = {} def add(pair): lcaddr = pair[1].lower() if d.has_key(lcaddr): return d[lcaddr] = pair new.append(pair) # List admin wants an explicit Reply-To: added if mlist.reply_goes_to_list == 2: add(parseaddr(mlist.reply_to_address)) # If we're not first stripping existing Reply-To: then we need to add # the original Reply-To:'s to the list we're building up. In both # cases we'll zap the existing field because RFC 2822 says max one is # allowed. if not mlist.first_strip_reply_to: orig = msg.get_all('reply-to', []) for pair in getaddresses(orig): add(pair) # Set Reply-To: header to point back to this list. Add this last # because some folks think that some MUAs make it easier to delete # addresses from the right than from the left. if mlist.reply_goes_to_list == 1: i18ndesc = uheader(mlist, mlist.description, 'Reply-To') # Added by Systers to be able to use the thread address when using reply-to list. if DlistUtils.enabled(mlist): thread = DlistUtils.Thread(mlist) add((str(i18ndesc), thread.getThreadAddress(msgdata))) else: add((str(i18ndesc), mlist.GetListEmail())) del msg['reply-to'] # Don't put Reply-To: back if there's nothing to add! if new: # Preserve order msg['Reply-To'] = COMMASPACE.join( [formataddr(pair) for pair in new]) # The To field normally contains the list posting address. However # when messages are fully personalized, that header will get # overwritten with the address of the recipient. We need to get the # posting address in one of the recipient headers or they won't be # able to reply back to the list. It's possible the posting address # was munged into the Reply-To header, but if not, we'll add it to a # Cc header. BAW: should we force it into a Reply-To header in the # above code? # Also skip Cc if this is an anonymous list as list posting address # is already in From and Reply-To in this case. if mlist.personalize == 2 and mlist.reply_goes_to_list <> 1 \ and not mlist.anonymous_list: # Watch out for existing Cc headers, merge, and remove dups. Note # that RFC 2822 says only zero or one Cc header is allowed. new = [] d = {} for pair in getaddresses(msg.get_all('cc', [])): add(pair) i18ndesc = uheader(mlist, mlist.description, 'Cc') add((str(i18ndesc), mlist.GetListEmail())) del msg['Cc'] msg['Cc'] = COMMASPACE.join([formataddr(pair) for pair in new]) # Add list-specific headers as defined in RFC 2369 and RFC 2919, but only # if the message is being crafted for a specific list (e.g. not for the # password reminders). # # BAW: Some people really hate the List-* headers. It seems that the free # version of Eudora (possibly on for some platforms) does not hide these # headers by default, pissing off their users. Too bad. Fix the MUAs. if msgdata.get('_nolist') or not mlist.include_rfc2369_headers: return # This will act like an email address for purposes of formataddr() listid = '%s.%s' % (mlist.internal_name(), mlist.host_name) cset = Utils.GetCharSet(mlist.preferred_language) if mlist.description: # Don't wrap the header since here we just want to get it properly RFC # 2047 encoded. i18ndesc = uheader(mlist, mlist.description, 'List-Id', maxlinelen=998) listid_h = formataddr((str(i18ndesc), listid)) else: # without desc we need to ensure the MUST brackets listid_h = '<%s>' % listid # We always add a List-ID: header. del msg['list-id'] msg['List-Id'] = listid_h # For internally crafted messages, we also add a (nonstandard), # "X-List-Administrivia: yes" header. For all others (i.e. those coming # from list posts), we add a bunch of other RFC 2369 headers. requestaddr = mlist.GetRequestEmail() subfieldfmt = '<%s>, <mailto:%s?subject=%ssubscribe>' listinfo = mlist.GetScriptURL('listinfo', absolute=1) useropts = mlist.GetScriptURL('options', absolute=1) headers = {} if msgdata.get('reduced_list_headers'): headers['X-List-Administrivia'] = 'yes' else: headers.update({ 'List-Help' : '<mailto:%s?subject=help>' % requestaddr, 'List-Unsubscribe': subfieldfmt % (useropts, requestaddr, 'un'), 'List-Subscribe' : subfieldfmt % (listinfo, requestaddr, ''), }) # List-Post: is controlled by a separate attribute if mlist.include_list_post_header: headers['List-Post'] = '<mailto:%s>' % mlist.GetListEmail() # Add this header if we're archiving if mlist.archive: archiveurl = mlist.GetBaseArchiveURL() if archiveurl.endswith('/'): archiveurl = archiveurl[:-1] headers['List-Archive'] = '<%s>' % archiveurl # First we delete any pre-existing headers because the RFC permits only # one copy of each, and we want to be sure it's ours. for h, v in headers.items(): del msg[h] # Wrap these lines if they are too long. 78 character width probably # shouldn't be hardcoded, but is at least text-MUA friendly. The # adding of 2 is for the colon-space separator. if len(h) + 2 + len(v) > 78: v = CONTINUATION.join(v.split(', ')) msg[h] = v
def addNewMember(self, member, **kws): if kws.has_key("affect_dlist_database"): affect_dlist_database = kws["affect_dlist_database"] del kws["affect_dlist_database"] else: affect_dlist_database = True if kws.has_key("digest"): digest = kws["digest"] else: digest = False subscriber = DlistUtils.Subscriber(self.__mlist) oldm.addNewMember(self, member, **kws) if affect_dlist_database: if DlistUtils.enabled(self.__mlist): subscriber.subscribeToList(member) if digest: subscriber.setDigest(member, 1) # if oldm.isMember(self,member): # raise Errors.MMAlreadyAMember, member database = self._dbconnect() store = Store(database) newMember = StormMembers() # Parse the keywords digest = 0 password = Utils.MakeRandomPassword() language = self.__mlist.preferred_language realname = None if kws.has_key('digest'): digest = kws['digest'] del kws['digest'] if kws.has_key('password'): password = kws['password'] del kws['password'] if kws.has_key('language'): language = kws['language'] del kws['language'] if kws.has_key('realname'): realname = kws['realname'] del kws['realname'] # Assert that no other keywords are present if kws: raise ValueError, kws.keys() newMember.delivery_status = MemberAdaptor.ENABLED try: newMember.listname = unicode(self.list,"utf-8") except: newMember.listname = self.list try: newMember.password = unicode(password,"utf-8") except: newMember.password = password try: newMember.lang = unicode(language,"utf-8") except: newMember.lang = language try: newMember.address = unicode(member,"utf-8") except: newMember.address = member if realname: try: newMember.name = unicode(realname,"utf-8") except: newMember.name = realname # Set the member's default set of options if self.__mlist.new_member_options: newMember.user_options = self.__mlist.new_member_options # If the localpart has uppercase letters in it, then the value in the # members (or digest_members) dict is the case preserved address. # Otherwise the value is 0. Note that the case of the domain part is # of course ignored. #if Utils.LCDomain(member) == member.lower(): # value = 0 #else: # value = member if digest: newMember.digest = u"Y" else: newMember.digest = u"N" store.add(newMember) store.commit()
def process(mlist, msg, msgdata): if msgdata.get('approved') or msgdata.get('fromusenet'): return # First of all, is the poster a member or not? for sender in msg.get_senders(): if mlist.isMember(sender): break else: if DlistUtils.enabled(mlist): # Check if this is an alias for a member of the list. alias = DlistUtils.Alias(mlist) sender = alias.canonicalize_sender(msg.get_senders()) else: sender = None if sender: # If the member's moderation flag is on, then perform the moderation # action. if mlist.getMemberOption(sender, mm_cfg.Moderate): # Note that for member_moderation_action, 0==Hold, 1=Reject, # 2==Discard if mlist.member_moderation_action == 0: # Hold. BAW: WIBNI we could add the member_moderation_notice # to the notice sent back to the sender? msgdata['sender'] = sender Hold.hold_for_approval(mlist, msg, msgdata, ModeratedMemberPost) elif mlist.member_moderation_action == 1: # Reject text = mlist.member_moderation_notice if text: text = Utils.wrap(text) else: # Use the default RejectMessage notice string text = None raise Errors.RejectMessage, text elif mlist.member_moderation_action == 2: # Discard. BAW: Again, it would be nice if we could send a # discard notice to the sender raise Errors.DiscardMessage else: assert 0, 'bad member_moderation_action' # Should we do anything explict to mark this message as getting past # this point? No, because further pipeline handlers will need to do # their own thing. return else: sender = msg.get_sender() # From here on out, we're dealing with non-members. listname = mlist.internal_name() if matches_p(sender, mlist.accept_these_nonmembers, listname): return if matches_p(sender, mlist.hold_these_nonmembers, listname): Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonMemberPost) # No return if matches_p(sender, mlist.reject_these_nonmembers, listname): do_reject(mlist) # No return if matches_p(sender, mlist.discard_these_nonmembers, listname): do_discard(mlist, msg) # No return # Okay, so the sender wasn't specified explicitly by any of the non-member # moderation configuration variables. Handle by way of generic non-member # action. assert 0 <= mlist.generic_nonmember_action <= 4 if mlist.generic_nonmember_action == 0: # Accept return elif mlist.generic_nonmember_action == 1: Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonMemberPost) elif mlist.generic_nonmember_action == 2: do_reject(mlist) elif mlist.generic_nonmember_action == 3: do_discard(mlist, msg)
def process(mlist, msg, msgdata): # Digests and Mailman-craft messages should not get additional headers if msgdata.get('isdigest') or msgdata.get('nodecorate'): return d = {} if msgdata.get('personalize'): # Calculate the extra personalization dictionary. Note that the # length of the recips list better be exactly 1. recips = msgdata.get('recips') assert type(recips) == ListType and len(recips) == 1 member = recips[0].lower() d['user_address'] = member try: d['user_delivered_to'] = mlist.getMemberCPAddress(member) # BAW: Hmm, should we allow this? d['user_password'] = mlist.getMemberPassword(member) d['user_language'] = mlist.getMemberLanguage(member) username = mlist.getMemberName(member) or None try: username = username.encode(Utils.GetCharSet(d['user_language'])) except (AttributeError, UnicodeError): username = member d['user_name'] = username d['user_optionsurl'] = mlist.GetOptionsURL(member) except Errors.NotAMemberError: pass # These strings are descriptive for the log file and shouldn't be i18n'd d.update(msgdata.get('decoration-data', {})) header = decorate(mlist, mlist.msg_header, 'non-digest header', d) # The footer for a dlist should look different compared to a non dlist. try: if DlistUtils.enabled(mlist): footer = decorate(mlist, msgdata['footer-text'], 'non-digest footer', d) else: footer = decorate(mlist, mlist.msg_footer, 'non-digest footer', d) except KeyError: syslog('error', 'footer in Decorate.py not found, entered empty string instead') footer = decorate(mlist, '', 'non-digest footer', d) # Better to have no footer than that the email doesn't get send due to a crash # Escape hatch if both the footer and header are empty if not header and not footer: return # Be MIME smart here. We only attach the header and footer by # concatenation when the message is a non-multipart of type text/plain. # Otherwise, if it is not a multipart, we make it a multipart, and then we # add the header and footer as text/plain parts. # # BJG: In addition, only add the footer if the message's character set # matches the charset of the list's preferred language. This is a # suboptimal solution, and should be solved by allowing a list to have # multiple headers/footers, for each language the list supports. # # Also, if the list's preferred charset is us-ascii, we can always # safely add the header/footer to a plain text message since all # charsets Mailman supports are strict supersets of us-ascii -- # no, UTF-16 emails are not supported yet. # # TK: Message with 'charset=' cause trouble. So, instead of # mgs.get_content_charset('us-ascii') ... mcset = msg.get_content_charset() or 'us-ascii' lcset = Utils.GetCharSet(mlist.preferred_language) msgtype = msg.get_content_type() # BAW: If the charsets don't match, should we add the header and footer by # MIME multipart chroming the message? wrap = True if not msg.is_multipart() and msgtype == 'text/plain': # TK: Try to keep the message plain by converting the header/ # footer/oldpayload into unicode and encode with mcset/lcset. # Try to decode qp/base64 also. uheader = unicode(header, lcset, 'ignore') ufooter = unicode(footer, lcset, 'ignore') try: oldpayload = unicode(msg.get_payload(decode=True), mcset) frontsep = endsep = u'' if header and not header.endswith('\n'): frontsep = u'\n' if footer and not oldpayload.endswith('\n'): endsep = u'\n' payload = uheader + frontsep + oldpayload + endsep + ufooter try: # first, try encode with list charset payload = payload.encode(lcset) newcset = lcset except UnicodeError: if lcset != mcset: # if fail, encode with message charset (if different) payload = payload.encode(mcset) newcset = mcset # if this fails, fallback to outer try and wrap=true format = msg.get_param('format') delsp = msg.get_param('delsp') del msg['content-transfer-encoding'] del msg['content-type'] msg.set_payload(payload, newcset) if format: msg.set_param('Format', format) if delsp: msg.set_param('DelSp', delsp) wrap = False except (LookupError, UnicodeError): pass elif msg.get_type() == 'multipart/mixed': # The next easiest thing to do is just prepend the header and append # the footer as additional subparts payload = msg.get_payload() if not isinstance(payload, ListType): payload = [payload] if footer: mimeftr = MIMEText(footer, 'plain', lcset) mimeftr['Content-Disposition'] = 'inline' payload.append(mimeftr) if header: mimehdr = MIMEText(header, 'plain', lcset) mimehdr['Content-Disposition'] = 'inline' payload.insert(0, mimehdr) msg.set_payload(payload) wrap = False # If we couldn't add the header or footer in a less intrusive way, we can # at least do it by MIME encapsulation. We want to keep as much of the # outer chrome as possible. if not wrap: return # Because of the way Message objects are passed around to process(), we # need to play tricks with the outer message -- i.e. the outer one must # remain the same instance. So we're going to create a clone of the outer # message, with all the header chrome intact, then copy the payload to it. # This will give us a clone of the original message, and it will form the # basis of the interior, wrapped Message. inner = Message() # Which headers to copy? Let's just do the Content-* headers for h, v in msg.items(): if h.lower().startswith('content-'): inner[h] = v inner.set_payload(msg.get_payload()) # For completeness inner.set_unixfrom(msg.get_unixfrom()) inner.preamble = msg.preamble inner.epilogue = msg.epilogue # Don't copy get_charset, as this might be None, even if # get_content_charset isn't. However, do make sure there is a default # content-type, even if the original message was not MIME. inner.set_default_type(msg.get_default_type()) # BAW: HACK ALERT. if hasattr(msg, '__version__'): inner.__version__ = msg.__version__ # Now, play games with the outer message to make it contain three # subparts: the header (if any), the wrapped message, and the footer (if # any). payload = [inner] if header: mimehdr = MIMEText(header, 'plain', lcset) mimehdr['Content-Disposition'] = 'inline' payload.insert(0, mimehdr) if footer: mimeftr = MIMEText(footer, 'plain', lcset) mimeftr['Content-Disposition'] = 'inline' payload.append(mimeftr) msg.set_payload(payload) del msg['content-type'] del msg['content-transfer-encoding'] del msg['content-disposition'] msg['Content-Type'] = 'multipart/mixed'