def __refuse(self, request, recip, comment, origmsg=None, lang=None): # As this message is going to the requestor, try to set the language # to his/her language choice, if they are a member. Otherwise use the # list's preferred language. realname = self.real_name if lang is None: lang = self.getMemberLanguage(recip) text = Utils.maketext('refuse.txt', { 'listname': realname, 'request': request, 'reason': comment, 'adminaddr': self.GetOwnerEmail(), }, lang=lang, mlist=self) otrans = i18n.get_translation() i18n.set_language(lang) try: # add in original message, but not wrap/filled if origmsg: text = NL.join([ text, '---------- ' + _('Original Message') + ' ----------', str(origmsg) ]) subject = _('Request to mailing list %(realname)s rejected') finally: i18n.set_translation(otrans) msg = Message.UserNotification(recip, self.GetOwnerEmail(), subject, text, lang) msg.send(self)
def __refuse(self, request, recip, comment, origmsg=None, lang=None): # As this message is going to the requestor, try to set the language # to his/her language choice, if they are a member. Otherwise use the # list's preferred language. realname = self.real_name if lang is None: lang = self.getMemberLanguage(recip) text = Utils.maketext( 'refuse.txt', {'listname' : realname, 'request' : request, 'reason' : comment, 'adminaddr': self.GetOwnerEmail(), }, lang=lang, mlist=self) otrans = i18n.get_translation() i18n.set_language(lang) try: # add in original message, but not wrap/filled if origmsg: text = NL.join( [text, '---------- ' + _('Original Message') + ' ----------', str(origmsg) ]) subject = _('Request to mailing list %(realname)s rejected') finally: i18n.set_translation(otrans) msg = Message.UserNotification(recip, self.GetOwnerEmail(), subject, text, lang) msg.send(self)
def decode_headers(self): """MIME-decode headers. If the email, subject, or author attributes contain non-ASCII characters using the encoded-word syntax of RFC 2047, decoded versions of those attributes are placed in the self.decoded (a dictionary). If the list's charset differs from the header charset, an attempt is made to decode the headers as Unicode. If that fails, they are left undecoded. """ author = self.decode_charset(self.author) subject = self.decode_charset(self.subject) if author: self.decoded['author'] = author email = self.decode_charset(self.email) if email: self.decoded['email'] = email if subject: if mm_cfg.ARCHIVER_OBSCURES_EMAILADDRS: otrans = i18n.get_translation() try: i18n.set_language(self._lang) atmark = unicode(_(' at '), Utils.GetCharSet(self._lang)) subject = re.sub(r'([-+,.\w]+)@([-+.\w]+)', '\g<1>' + atmark + '\g<2>', subject) finally: i18n.set_translation(otrans) self.decoded['subject'] = subject self.decoded['stripped'] = self.strip_subject(subject or self.subject)
def html_foot(self): # avoid i18n side-effects mlist = self.maillist otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) # Convenience def quotetime(s): return html_quote(i18n.ctime(s), self.lang) try: d = {"lastdate": quotetime(self.lastdate), "archivedate": quotetime(self.archivedate), "listinfo": mlist.GetScriptURL('listinfo', absolute=1), "version": self.version, "listname": html_quote(mlist.real_name, self.lang), } i = {"thread": _("thread"), "subject": _("subject"), "author": _("author"), "date": _("date") } finally: i18n.set_translation(otrans) for t in i.keys(): cap = t[0].upper() + t[1:] if self.type == cap: d["%s_ref" % (t)] = "" else: d["%s_ref" % (t)] = ('<a href="%s.html#start">[ %s ]</a>' % (t, i[t])) return quick_maketext( 'archidxfoot.html', d, mlist=mlist)
def html_foot(self): # avoid i18n side-effects mlist = self.maillist otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) # Convenience def quotetime(s): return html_quote(i18n.ctime(s), self.lang) try: d = { "lastdate": quotetime(self.lastdate), "archivedate": quotetime(self.archivedate), "listinfo": mlist.GetScriptURL('listinfo', absolute=1), "version": self.version, "listname": html_quote(mlist.real_name, self.lang), } i = { "thread": _("thread"), "subject": _("subject"), "author": _("author"), "date": _("date") } finally: i18n.set_translation(otrans) for t in i.keys(): cap = t[0].upper() + t[1:] if self.type == cap: d["%s_ref" % (t)] = "" else: d["%s_ref" % (t)] = ('<a href="%s.html#start">[ %s ]</a>' % (t, i[t])) return quick_maketext('archidxfoot.html', d, mlist=mlist)
def sendProbe(self, member, msg): listname = self.real_name # Put together the substitution dictionary. d = { "listname": listname, "address": member, "optionsurl": self.GetOptionsURL(member, absolute=True), "owneraddr": self.GetOwnerEmail(), } text = Utils.maketext("probe.txt", d, lang=self.getMemberLanguage(member), mlist=self) # Calculate the VERP'd sender address for bounce processing of the # probe message. token = self.pend_new(Pending.PROBE_BOUNCE, member, msg) probedict = {"bounces": self.internal_name() + "-bounces", "token": token} probeaddr = "%s@%s" % ((mm_cfg.VERP_PROBE_FORMAT % probedict), self.host_name) # Calculate the Subject header, in the member's preferred language ulang = self.getMemberLanguage(member) otrans = i18n.get_translation() i18n.set_language(ulang) try: subject = _("%(listname)s mailing list probe message") finally: i18n.set_translation(otrans) outer = Message.UserNotification(member, probeaddr, subject, lang=ulang) outer.set_type("multipart/mixed") text = MIMEText(text, _charset=Utils.GetCharSet(ulang)) outer.attach(text) outer.attach(MIMEMessage(msg)) # Turn off further VERP'ing in the final delivery step. We set # probe_token for the OutgoingRunner to more easily handling local # rejects of probe messages. outer.send(self, envsender=probeaddr, verp=False, probe_token=token)
def as_html(self): """Different attributes that can be used when generating a html file from a template for the archives.""" d = self.__dict__.copy() # avoid i18n side-effects otrans = i18n.get_translation() i18n.set_language(self._lang) try: d["prev"], d["prev_wsubj"] = self._get_prev() d["next"], d["next_wsubj"] = self._get_next() d["email_html"] = self.quote(self.email) d["title"] = self.quote(self.subject) d["subject_html"] = self.quote(self.subject) # TK: These two _url variables are used to compose a response # from the archive web page. So, ... # Possibly remove since not used?? d["subject_url"] = url_quote('Re: ' + self.subject) d["in_reply_to_url"] = url_quote(self._message_id) if mm_cfg.ARCHIVER_OBSCURES_EMAILADDRS: # Point the mailto url back to the list author = re.sub('@', _(' at '), self.author) emailurl = self._mlist.GetListEmail() else: author = self.author emailurl = self.email d["author_html"] = self.quote(author) d["email_url"] = url_quote(emailurl) d["datestr_html"] = self.quote(i18n.ctime(int(self.date))) d["body"] = self._get_body() d['listurl'] = self._mlist.GetScriptURL('listinfo', absolute=1) d['listname'] = self._mlist.real_name d['encoding'] = '' try: # This should work after a couple of emails has been send and the thread # address has been set, but to not get an error in the beginning the error # is caught and an already existing address is provided. Is for dlists. d['thread_addr'] = self.thread_addr d['thread_addr_at'] = re.sub('@', _(' at '), self.thread_addr) except: d['thread_addr'] = d["email_url"] d['thread_addr_at'] = d["email_url"] finally: i18n.set_translation(otrans) charset = Utils.GetCharSet(self._lang) d["encoding"] = html_charset % charset self._add_decoded(d) return quick_maketext( 'article.html', d, lang=self._lang, mlist=self._mlist)
def SendHostileSubscriptionNotice(self, listname, address): # Some one was invited to one list but tried to confirm to a different # list. We inform both list owners of the bogosity, but be careful # not to reveal too much information. selfname = self.internal_name() syslog("mischief", "%s was invited to %s but confirmed to %s", address, listname, selfname) # First send a notice to the attacked list msg = Message.OwnerNotification( self, _("Hostile subscription attempt detected"), Utils.wrap( _( """%(address)s was invited to a different mailing list, but in a deliberate malicious attempt they tried to confirm the invitation to your list. We just thought you'd like to know. No further action by you is required.""" ) ), ) msg.send(self) # Now send a notice to the invitee list try: # Avoid import loops from Mailman.MailList import MailList mlist = MailList(listname, lock=False) except Errors.MMListError: # Oh well return otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) try: msg = Message.OwnerNotification( mlist, _("Hostile subscription attempt detected"), Utils.wrap( _( """You invited %(address)s to your list, but in a deliberate malicious attempt, they tried to confirm the invitation to a different list. We just thought you'd like to know. No further action by you is required.""" ) ), ) msg.send(mlist) finally: i18n.set_translation(otrans)
def _onefile(self, msg, msgdata): # Do some common sanity checking on the message metadata. It's got to # be destined for a particular mailing list. This switchboard is used # to shunt off badly formatted messages. We don't want to just trash # them because they may be fixable with human intervention. Just get # them out of our site though. # # Find out which mailing list this message is destined for. listname = msgdata.get('listname') if not listname: listname = mm_cfg.MAILMAN_SITE_LIST mlist = self._open_list(listname) if not mlist: syslog('error', 'Dequeuing message destined for missing list: %s', listname) self._shunt.enqueue(msg, msgdata) return # Now process this message, keeping track of any subprocesses that may # have been spawned. We'll reap those later. # # We also want to set up the language context for this message. The # context will be the preferred language for the user if a member of # the list, or the list's preferred language. However, we must take # special care to reset the defaults, otherwise subsequent messages # may be translated incorrectly. BAW: I'm not sure I like this # approach, but I can't think of anything better right now. otranslation = i18n.get_translation() sender = msg.get_sender() if mlist: lang = mlist.getMemberLanguage(sender) else: lang = mm_cfg.DEFAULT_SERVER_LANGUAGE i18n.set_language(lang) msgdata['lang'] = lang try: keepqueued = self._dispose(mlist, msg, msgdata) finally: i18n.set_translation(otranslation) # Keep tabs on any child processes that got spawned. kids = msgdata.get('_kids') if kids: self._kids.update(kids) if keepqueued: self._switchboard.enqueue(msg, msgdata)
def html_TOC(self): mlist = self.maillist listname = mlist.internal_name() mbox = os.path.join(mlist.archive_dir()+'.mbox', listname+'.mbox') d = {"listname": mlist.real_name, "listinfo": mlist.GetScriptURL('listinfo', absolute=1), "fullarch": '../%s.mbox/%s.mbox' % (listname, listname), "size": sizeof(mbox, mlist.preferred_language), 'meta': '', } # Avoid i18n side-effects otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) try: if not self.archives: d["noarchive_msg"] = _( '<P>Currently, there are no archives. </P>') d["archive_listing_start"] = "" d["archive_listing_end"] = "" d["archive_listing"] = "" else: d["noarchive_msg"] = "" d["archive_listing_start"] = quick_maketext( 'archliststart.html', lang=mlist.preferred_language, mlist=mlist) d["archive_listing_end"] = quick_maketext( 'archlistend.html', mlist=mlist) accum = [] for a in self.archives: accum.append(self.html_TOC_entry(a)) d["archive_listing"] = EMPTYSTRING.join(accum) finally: i18n.set_translation(otrans) # The TOC is always in the charset of the list's preferred language d['meta'] += html_charset % Utils.GetCharSet(mlist.preferred_language) # The site can disable public access to the mbox file. if mm_cfg.PUBLIC_MBOX: template = 'archtoc.html' else: template = 'archtocnombox.html' return quick_maketext(template, d, mlist=mlist)
def html_TOC(self): mlist = self.maillist listname = mlist.internal_name() mbox = os.path.join(mlist.archive_dir() + '.mbox', listname + '.mbox') d = { "listname": mlist.real_name, "listinfo": mlist.GetScriptURL('listinfo', absolute=1), "fullarch": '../%s.mbox/%s.mbox' % (listname, listname), "size": sizeof(mbox, mlist.preferred_language), 'meta': '', } # Avoid i18n side-effects otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) try: if not self.archives: d["noarchive_msg"] = _( '<P>Currently, there are no archives. </P>') d["archive_listing_start"] = "" d["archive_listing_end"] = "" d["archive_listing"] = "" else: d["noarchive_msg"] = "" d["archive_listing_start"] = quick_maketext( 'archliststart.html', lang=mlist.preferred_language, mlist=mlist) d["archive_listing_end"] = quick_maketext('archlistend.html', mlist=mlist) accum = [] for a in self.archives: accum.append(self.html_TOC_entry(a)) d["archive_listing"] = EMPTYSTRING.join(accum) finally: i18n.set_translation(otrans) # The TOC is always in the charset of the list's preferred language d['meta'] += html_charset % Utils.GetCharSet(mlist.preferred_language) # The site can disable public access to the mbox file. if mm_cfg.PUBLIC_MBOX: template = 'archtoc.html' else: template = 'archtocnombox.html' return quick_maketext(template, d, mlist=mlist)
def html_head(self): # avoid i18n side-effects mlist = self.maillist otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) # Convenience def quotetime(s): return html_quote(i18n.ctime(s), self.lang) try: d = { "listname": html_quote(mlist.real_name, self.lang), "archtype": self.type, "archive": self.volNameToDesc(self.archive), "listinfo": mlist.GetScriptURL('listinfo', absolute=1), "firstdate": quotetime(self.firstdate), "lastdate": quotetime(self.lastdate), "size": self.size, } i = { "thread": _("thread"), "subject": _("subject"), "author": _("author"), "date": _("date"), } finally: i18n.set_translation(otrans) for t in i.keys(): cap = t[0].upper() + t[1:] if self.type == cap: d["%s_ref" % (t)] = "" d["archtype"] = i[t] else: d["%s_ref" % (t)] = ('<a href="%s.html#start">[ %s ]</a>' % (t, i[t])) if self.charset: d["encoding"] = html_charset % self.charset else: d["encoding"] = "" return quick_maketext('archidxhead.html', d, mlist=mlist)
def sendProbe(self, member, msg): listname = self.real_name # Put together the substitution dictionary. d = { 'listname': listname, 'address': member, 'optionsurl': self.GetOptionsURL(member, absolute=True), 'owneraddr': self.GetOwnerEmail(), } text = Utils.maketext('probe.txt', d, lang=self.getMemberLanguage(member), mlist=self) # Calculate the VERP'd sender address for bounce processing of the # probe message. token = self.pend_new(Pending.PROBE_BOUNCE, member, msg) probedict = { 'bounces': self.internal_name() + '-bounces', 'token': token, } probeaddr = '%s@%s' % ( (mm_cfg.VERP_PROBE_FORMAT % probedict), self.host_name) # Calculate the Subject header, in the member's preferred language ulang = self.getMemberLanguage(member) otrans = i18n.get_translation() i18n.set_language(ulang) try: subject = _('%(listname)s mailing list probe message') finally: i18n.set_translation(otrans) outer = Message.UserNotification(member, probeaddr, subject, lang=ulang) outer.set_type('multipart/mixed') text = MIMEText(text, _charset=Utils.GetCharSet(ulang)) outer.attach(text) outer.attach(MIMEMessage(msg)) # Turn off further VERP'ing in the final delivery step. We set # probe_token for the OutgoingRunner to more easily handling local # rejects of probe messages. outer.send(self, envsender=probeaddr, verp=False, probe_token=token)
def as_html(self): d = self.__dict__.copy() # avoid i18n side-effects otrans = i18n.get_translation() i18n.set_language(self._lang) try: d["prev"], d["prev_wsubj"] = self._get_prev() d["next"], d["next_wsubj"] = self._get_next() d["email_html"] = self.quote(self.email) d["title"] = self.quote(self.subject) d["subject_html"] = self.quote(self.subject) d["message_id"] = self.quote(self._message_id) # TK: These two _url variables are used to compose a response # from the archive web page. So, ... d["subject_url"] = url_quote('Re: ' + self.subject) d["in_reply_to_url"] = url_quote(self._message_id) if mm_cfg.ARCHIVER_OBSCURES_EMAILADDRS: # Point the mailto url back to the list author = re.sub('@', _(' at '), self.author) emailurl = self._mlist.GetListEmail() else: author = self.author emailurl = self.email d["author_html"] = self.quote(author) d["email_url"] = url_quote(emailurl) d["datestr_html"] = self.quote(i18n.ctime(int(self.date))) d["body"] = self._get_body() d['listurl'] = self._mlist.GetScriptURL('listinfo', absolute=1) d['listname'] = self._mlist.real_name d['encoding'] = '' finally: i18n.set_translation(otrans) charset = Utils.GetCharSet(self._lang) d["encoding"] = html_charset % charset self._add_decoded(d) return quick_maketext('article.html', d, lang=self._lang, mlist=self._mlist)
def as_html(self): d = self.__dict__.copy() # avoid i18n side-effects otrans = i18n.get_translation() i18n.set_language(self._lang) try: d["prev"], d["prev_wsubj"] = self._get_prev() d["next"], d["next_wsubj"] = self._get_next() d["email_html"] = self.quote(self.email) d["title"] = self.quote(self.subject) d["subject_html"] = self.quote(self.subject) d["message_id"] = self.quote(self._message_id) # TK: These two _url variables are used to compose a response # from the archive web page. So, ... d["subject_url"] = url_quote('Re: ' + self.subject) d["in_reply_to_url"] = url_quote(self._message_id) if mm_cfg.ARCHIVER_OBSCURES_EMAILADDRS: # Point the mailto url back to the list author = re.sub('@', _(' at '), self.author) emailurl = self._mlist.GetListEmail() else: author = self.author emailurl = self.email d["author_html"] = self.quote(author) d["email_url"] = url_quote(emailurl) d["datestr_html"] = self.quote(i18n.ctime(int(self.date))) d["body"] = self._get_body() d['listurl'] = self._mlist.GetScriptURL('listinfo', absolute=1) d['listname'] = self._mlist.real_name d['encoding'] = '' finally: i18n.set_translation(otrans) charset = Utils.GetCharSet(self._lang) d["encoding"] = html_charset % charset self._add_decoded(d) return quick_maketext( 'article.html', d, lang=self._lang, mlist=self._mlist)
def html_head(self): # avoid i18n side-effects mlist = self.maillist otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) # Convenience def quotetime(s): return html_quote(i18n.ctime(s), self.lang) try: d = {"listname": html_quote(mlist.real_name, self.lang), "archtype": self.type, "archive": self.volNameToDesc(self.archive), "listinfo": mlist.GetScriptURL('listinfo', absolute=1), "firstdate": quotetime(self.firstdate), "lastdate": quotetime(self.lastdate), "size": self.size, } i = {"thread": _("thread"), "subject": _("subject"), "author": _("author"), "date": _("date"), } finally: i18n.set_translation(otrans) for t in i.keys(): cap = t[0].upper() + t[1:] if self.type == cap: d["%s_ref" % (t)] = "" d["archtype"] = i[t] else: d["%s_ref" % (t)] = ('<a href="%s.html#start">[ %s ]</a>' % (t, i[t])) if self.charset: d["encoding"] = html_charset % self.charset else: d["encoding"] = "" return quick_maketext( 'archidxhead.html', d, mlist=mlist)
def as_text(self): d = self.__dict__.copy() # We need to guarantee a valid From_ line, even if there are # bososities in the headers. if not d.get('fromdate', '').strip(): d['fromdate'] = time.ctime(time.time()) if not d.get('email', '').strip(): d['email'] = '*****@*****.**' if not d.get('datestr', '').strip(): d['datestr'] = time.ctime(time.time()) # headers = ['From %(email)s %(fromdate)s', 'From: %(email)s (%(author)s)', 'Date: %(datestr)s', 'Subject: %(subject)s'] if d['_in_reply_to']: headers.append('In-Reply-To: %(_in_reply_to)s') if d['_references']: headers.append('References: %(_references)s') if d['_message_id']: headers.append('Message-ID: %(_message_id)s') body = EMPTYSTRING.join(self.body) cset = Utils.GetCharSet(self._lang) # Coerce the body to Unicode and replace any invalid characters. if not isinstance(body, types.UnicodeType): body = unicode(body, cset, 'replace') if mm_cfg.ARCHIVER_OBSCURES_EMAILADDRS: otrans = i18n.get_translation() try: i18n.set_language(self._lang) atmark = unicode(_(' at '), cset) body = re.sub(r'([-+,.\w]+)@([-+.\w]+)', '\g<1>' + atmark + '\g<2>', body) finally: i18n.set_translation(otrans) # Return body to character set of article. body = body.encode(cset, 'replace') return NL.join(headers) % d + '\n\n' + body + '\n'
def send_digests(mlist, mboxfp): # Set the digest volume and time if mlist.digest_last_sent_at: bump = False # See if we should bump the digest volume number timetup = time.localtime(mlist.digest_last_sent_at) now = time.localtime(time.time()) freq = mlist.digest_volume_frequency if freq == 0 and timetup[0] < now[0]: # Yearly bump = True elif freq == 1 and timetup[1] <> now[1]: # Monthly, but we take a cheap way to calculate this. We assume # that the clock isn't going to be reset backwards. bump = True elif freq == 2 and (timetup[1] % 4 <> now[1] % 4): # Quarterly, same caveat bump = True elif freq == 3: # Once again, take a cheap way of calculating this weeknum_last = int(time.strftime('%W', timetup)) weeknum_now = int(time.strftime('%W', now)) if weeknum_now > weeknum_last or timetup[0] > now[0]: bump = True elif freq == 4 and timetup[7] <> now[7]: # Daily bump = True if bump: mlist.bump_digest_volume() mlist.digest_last_sent_at = time.time() # Wrapper around actually digest crafter to set up the language context # properly. All digests are translated to the list's preferred language. otranslation = i18n.get_translation() i18n.set_language(mlist.preferred_language) try: send_i18n_digests(mlist, mboxfp) finally: i18n.set_translation(otranslation)
def as_text(self): d = self.__dict__.copy() # We need to guarantee a valid From_ line, even if there are # bososities in the headers. if not d.get('fromdate', '').strip(): d['fromdate'] = time.ctime(time.time()) if not d.get('email', '').strip(): d['email'] = '*****@*****.**' if not d.get('datestr', '').strip(): d['datestr'] = time.ctime(time.time()) # headers = [ 'From %(email)s %(fromdate)s', 'From: %(email)s (%(author)s)', 'Date: %(datestr)s', 'Subject: %(subject)s' ] if d['_in_reply_to']: headers.append('In-Reply-To: %(_in_reply_to)s') if d['_references']: headers.append('References: %(_references)s') if d['_message_id']: headers.append('Message-ID: %(_message_id)s') body = EMPTYSTRING.join(self.body) cset = Utils.GetCharSet(self._lang) # Coerce the body to Unicode and replace any invalid characters. if not isinstance(body, types.UnicodeType): body = unicode(body, cset, 'replace') if mm_cfg.ARCHIVER_OBSCURES_EMAILADDRS: otrans = i18n.get_translation() try: i18n.set_language(self._lang) atmark = unicode(_(' at '), cset) body = re.sub(r'([-+,.\w]+)@([-+.\w]+)', '\g<1>' + atmark + '\g<2>', body) finally: i18n.set_translation(otrans) # Return body to character set of article. body = body.encode(cset, 'replace') return NL.join(headers) % d + '\n\n' + body + '\n'
def SendHostileSubscriptionNotice(self, listname, address): # Some one was invited to one list but tried to confirm to a different # list. We inform both list owners of the bogosity, but be careful # not to reveal too much information. selfname = self.internal_name() syslog('mischief', '%s was invited to %s but confirmed to %s', address, listname, selfname) # First send a notice to the attacked list msg = Message.OwnerNotification( self, _('Hostile subscription attempt detected'), Utils.wrap( _("""%(address)s was invited to a different mailing list, but in a deliberate malicious attempt they tried to confirm the invitation to your list. We just thought you'd like to know. No further action by you is required."""))) msg.send(self) # Now send a notice to the invitee list try: # Avoid import loops from Mailman.MailList import MailList mlist = MailList(listname, lock=False) except Errors.MMListError: # Oh well return otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) try: msg = Message.OwnerNotification( mlist, _('Hostile subscription attempt detected'), Utils.wrap( _("""You invited %(address)s to your list, but in a deliberate malicious attempt, they tried to confirm the invitation to a different list. We just thought you'd like to know. No further action by you is required."""))) msg.send(mlist) finally: i18n.set_translation(otrans)
def prefix_subject(mlist, msg, msgdata): # Add the subject prefix unless the message is a digest or is being fast # tracked (e.g. internally crafted, delivered to a single user such as the # list admin). prefix = mlist.subject_prefix.strip() if not prefix: return subject = msg.get('subject', '') # Try to figure out what the continuation_ws is for the header if isinstance(subject, Header): lines = str(subject).splitlines() else: lines = subject.splitlines() ws = ' ' if len(lines) > 1 and lines[1] and lines[1][0] in ' \t': ws = lines[1][0] msgdata['origsubj'] = subject # The subject may be multilingual but we take the first charset as major # one and try to decode. If it is decodable, returned subject is in one # line and cset is properly set. If fail, subject is mime-encoded and # cset is set as us-ascii. See detail for ch_oneline() (CookHeaders one # line function). subject, cset = ch_oneline(subject) # TK: Python interpreter has evolved to be strict on ascii charset code # range. It is safe to use unicode string when manupilating header # contents with re module. It would be best to return unicode in # ch_oneline() but here is temporary solution. subject = unicode(subject, cset) # If the subject_prefix contains '%d', it is replaced with the # mailing list sequential number. Sequential number format allows # '%d' or '%05d' like pattern. prefix_pattern = re.escape(prefix) # unescape '%' :-< prefix_pattern = '%'.join(prefix_pattern.split(r'\%')) p = re.compile('%\d*d') if p.search(prefix, 1): # prefix have number, so we should search prefix w/number in subject. # Also, force new style. prefix_pattern = p.sub(r'\s*\d+\s*', prefix_pattern) old_style = False else: old_style = mm_cfg.OLD_STYLE_PREFIXING subject = re.sub(prefix_pattern, '', subject) # Previously the following re didn't have the first \s*. It would fail # if the incoming Subject: was like '[prefix] Re: Re: Re:' because of the # leading space after stripping the prefix. It is not known what MUA would # create such a Subject:, but the issue was reported. rematch = re.match('(\s*(RE|AW|SV|VS)\s*(\[\d+\])?\s*:\s*)+', subject, re.I) if rematch: subject = subject[rematch.end():] recolon = 'Re:' else: recolon = '' # Strip leading and trailing whitespace from subject. subject = subject.strip() # At this point, subject may become null if someone post mail with # Subject: [subject prefix] if subject == '': # We want the i18n context to be the list's preferred_language. It # could be the poster's. otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) subject = _('(no subject)') i18n.set_translation(otrans) cset = Utils.GetCharSet(mlist.preferred_language) subject = unicode(subject, cset) # and substitute %d in prefix with post_id try: prefix = prefix % mlist.post_id except TypeError: pass # If charset is 'us-ascii', try to concatnate as string because there # is some weirdness in Header module (TK) if cset == 'us-ascii': try: if old_style: h = u' '.join([recolon, prefix, subject]) else: if recolon: h = u' '.join([prefix, recolon, subject]) else: h = u' '.join([prefix, subject]) h = h.encode('us-ascii') h = uheader(mlist, h, 'Subject', continuation_ws=ws) change_header('Subject', h, mlist, msg, msgdata) ss = u' '.join([recolon, subject]) ss = ss.encode('us-ascii') ss = uheader(mlist, ss, 'Subject', continuation_ws=ws) msgdata['stripped_subject'] = ss return except UnicodeError: pass # Get the header as a Header instance, with proper unicode conversion # Because of rfc2047 encoding, spaces between encoded words can be # insignificant, so we need to append spaces to our encoded stuff. prefix += ' ' if recolon: recolon += ' ' if old_style: h = uheader(mlist, recolon, 'Subject', continuation_ws=ws) h.append(prefix) else: h = uheader(mlist, prefix, 'Subject', continuation_ws=ws) h.append(recolon) # TK: Subject is concatenated and unicode string. subject = subject.encode(cset, 'replace') h.append(subject, cset) change_header('Subject', h, mlist, msg, msgdata) ss = uheader(mlist, recolon, 'Subject', continuation_ws=ws) ss.append(subject, cset) msgdata['stripped_subject'] = ss
def prefix_subject(mlist, msg, msgdata): # Add the subject prefix unless the message is a digest or is being fast # tracked (e.g. internally crafted, delivered to a single user such as the # list admin). prefix = mlist.subject_prefix.strip() if not prefix: return subject = msg.get('subject', '') # Try to figure out what the continuation_ws is for the header if isinstance(subject, Header): lines = str(subject).splitlines() else: lines = subject.splitlines() ws = ' ' if len(lines) > 1 and lines[1] and lines[1][0] in ' \t': ws = lines[1][0] msgdata['origsubj'] = subject # The subject may be multilingual but we take the first charset as major # one and try to decode. If it is decodable, returned subject is in one # line and cset is properly set. If fail, subject is mime-encoded and # cset is set as us-ascii. See detail for ch_oneline() (CookHeaders one # line function). subject, cset = ch_oneline(subject) # TK: Python interpreter has evolved to be strict on ascii charset code # range. It is safe to use unicode string when manupilating header # contents with re module. It would be best to return unicode in # ch_oneline() but here is temporary solution. subject = unicode(subject, cset) # If the subject_prefix contains '%d', it is replaced with the # mailing list sequential number. Sequential number format allows # '%d' or '%05d' like pattern. prefix_pattern = re.escape(prefix) # unescape '%' :-< prefix_pattern = '%'.join(prefix_pattern.split(r'\%')) p = re.compile('%\d*d') if p.search(prefix, 1): # prefix have number, so we should search prefix w/number in subject. # Also, force new style. prefix_pattern = p.sub(r'\s*\d+\s*', prefix_pattern) old_style = False else: old_style = mm_cfg.OLD_STYLE_PREFIXING subject = re.sub(prefix_pattern, '', subject) # Previously the following re didn't have the first \s*. It would fail # if the incoming Subject: was like '[prefix] Re: Re: Re:' because of the # leading space after stripping the prefix. It is not known what MUA would # create such a Subject:, but the issue was reported. rematch = re.match( '(\s*(RE|AW|SV|VS)\s*(\[\d+\])?\s*:\s*)+', subject, re.I) if rematch: subject = subject[rematch.end():] recolon = 'Re:' else: recolon = '' # Strip leading and trailing whitespace from subject. subject = subject.strip() # At this point, subject may become null if someone post mail with # Subject: [subject prefix] if subject == '': # We want the i18n context to be the list's preferred_language. It # could be the poster's. otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) subject = _('(no subject)') i18n.set_translation(otrans) cset = Utils.GetCharSet(mlist.preferred_language) subject = unicode(subject, cset) # and substitute %d in prefix with post_id try: prefix = prefix % mlist.post_id except TypeError: pass # If charset is 'us-ascii', try to concatnate as string because there # is some weirdness in Header module (TK) if cset == 'us-ascii': try: if old_style: h = u' '.join([recolon, prefix, subject]) else: if recolon: h = u' '.join([prefix, recolon, subject]) else: h = u' '.join([prefix, subject]) h = h.encode('us-ascii') h = uheader(mlist, h, 'Subject', continuation_ws=ws) change_header('Subject', h, mlist, msg, msgdata) ss = u' '.join([recolon, subject]) ss = ss.encode('us-ascii') ss = uheader(mlist, ss, 'Subject', continuation_ws=ws) msgdata['stripped_subject'] = ss return except UnicodeError: pass # Get the header as a Header instance, with proper unicode conversion # Because of rfc2047 encoding, spaces between encoded words can be # insignificant, so we need to append spaces to our encoded stuff. prefix += ' ' if recolon: recolon += ' ' if old_style: h = uheader(mlist, recolon, 'Subject', continuation_ws=ws) h.append(prefix) else: h = uheader(mlist, prefix, 'Subject', continuation_ws=ws) h.append(recolon) # TK: Subject is concatenated and unicode string. subject = subject.encode(cset, 'replace') h.append(subject, cset) change_header('Subject', h, mlist, msg, msgdata) ss = uheader(mlist, recolon, 'Subject', continuation_ws=ws) ss.append(subject, cset) msgdata['stripped_subject'] = ss
def __init__(self, message=None, sequence=0, keepHeaders=[], lang=mm_cfg.DEFAULT_SERVER_LANGUAGE, mlist=None): self.__super_init(message, sequence, keepHeaders) self.prev = None self.next = None # Trim Re: from the subject line i = 0 while i != -1: result = REpat.match(self.subject) if result: i = result.end(0) self.subject = self.subject[i:] else: i = -1 # Useful to keep around self._lang = lang self._mlist = mlist if mm_cfg.ARCHIVER_OBSCURES_EMAILADDRS: # Avoid i18n side-effects. Note that the language for this # article (for this list) could be different from the site-wide # preferred language, so we need to ensure no side-effects will # occur. Think what happens when executing bin/arch. otrans = i18n.get_translation() try: i18n.set_language(lang) if self.author == self.email: self.author = self.email = re.sub('@', _(' at '), self.email) else: self.email = re.sub('@', _(' at '), self.email) finally: i18n.set_translation(otrans) # Snag the content-* headers. RFC 1521 states that their values are # case insensitive. ctype = message.get('Content-Type', 'text/plain') cenc = message.get('Content-Transfer-Encoding', '') self.ctype = ctype.lower() self.cenc = cenc.lower() self.decoded = {} cset = Utils.GetCharSet(mlist.preferred_language) cset_out = Charset(cset).output_charset or cset if isinstance(cset_out, unicode): # email 3.0.1 (python 2.4) doesn't like unicode cset_out = cset_out.encode('us-ascii') charset = message.get_content_charset(cset_out) if charset: charset = charset.lower().strip() if charset[0]=='"' and charset[-1]=='"': charset = charset[1:-1] if charset[0]=="'" and charset[-1]=="'": charset = charset[1:-1] try: body = message.get_payload(decode=True) except binascii.Error: body = None if body and charset != Utils.GetCharSet(self._lang): # decode body try: body = unicode(body, charset) except (UnicodeError, LookupError): body = None if body: self.body = [l + "\n" for l in body.splitlines()] self.decode_headers()
def hold_for_approval(mlist, msg, msgdata, exc): # BAW: This should really be tied into the email confirmation system so # that the message can be approved or denied via email as well as the # web. # # XXX We use the weird type(type) construct below because in Python 2.1, # type is a function not a type and so can't be used as the second # argument in isinstance(). However, in Python 2.5, exceptions are # new-style classes and so are not of ClassType. # FIXME pzv if isinstance(exc, type) or isinstance(exc, type(type)): # Go ahead and instantiate it now. exc = exc() listname = mlist.real_name sender = msgdata.get('sender', msg.get_sender()) usersubject = msg.get('subject') charset = Utils.GetCharSet(mlist.preferred_language) if usersubject: usersubject = Utils.oneline(usersubject, charset) else: usersubject = _('(no subject)') message_id = msg.get('message-id', 'n/a') owneraddr = mlist.GetOwnerEmail() adminaddr = mlist.GetBouncesEmail() requestaddr = mlist.GetRequestEmail() # We need to send both the reason and the rejection notice through the # translator again, because of the games we play above reason = Utils.wrap(exc.reason_notice()) if isinstance(exc, NonMemberPost) and mlist.nonmember_rejection_notice: msgdata['rejection_notice'] = Utils.wrap( mlist.nonmember_rejection_notice.replace('%(listowner)s', owneraddr)) else: msgdata['rejection_notice'] = Utils.wrap(exc.rejection_notice(mlist)) id = mlist.HoldMessage(msg, reason, msgdata) # Now we need to craft and send a message to the list admin so they can # deal with the held message. d = { 'listname': listname, 'hostname': mlist.host_name, 'reason': _(reason), 'sender': sender, 'subject': usersubject, 'admindb_url': mlist.GetScriptURL('admindb', absolute=1), } # We may want to send a notification to the original sender too fromusenet = msgdata.get('fromusenet') # Since we're sending two messages, which may potentially be in different # languages (the user's preferred and the list's preferred for the admin), # we need to play some i18n games here. Since the current language # context ought to be set up for the user, let's craft his message first. cookie = mlist.pend_new(Pending.HELD_MESSAGE, id) if not fromusenet and ackp(msg) and mlist.respond_to_post_requests and \ mlist.autorespondToSender(sender, mlist.getMemberLanguage(sender)): # Get a confirmation cookie d['confirmurl'] = '%s/%s' % (mlist.GetScriptURL('confirm', absolute=1), cookie) lang = msgdata.get('lang', mlist.getMemberLanguage(sender)) subject = _('Your message to %(listname)s awaits moderator approval') text = Utils.maketext('postheld.txt', d, lang=lang, mlist=mlist) nmsg = Message.UserNotification(sender, owneraddr, subject, text, lang) nmsg.send(mlist) # Now the message for the list owners. Be sure to include the list # moderators in this message. This one should appear to come from # <list>-owner since we really don't need to do bounce processing on it. if mlist.admin_immed_notify: # Now let's temporarily set the language context to that which the # admin is expecting. otranslation = i18n.get_translation() i18n.set_language(mlist.preferred_language) try: lang = mlist.preferred_language charset = Utils.GetCharSet(lang) # We need to regenerate or re-translate a few values in d d['reason'] = _(reason) d['subject'] = usersubject # craft the admin notification message and deliver it subject = _('%(listname)s post from %(sender)s requires approval') nmsg = Message.UserNotification(owneraddr, owneraddr, subject, lang=lang) nmsg.set_type('multipart/mixed') text = MIMEText(Utils.maketext('postauth.txt', d, raw=1, mlist=mlist), _charset=charset) dmsg = MIMEText(Utils.wrap( _("""\ If you reply to this message, keeping the Subject: header intact, Mailman will discard the held message. Do this if the message is spam. If you reply to this message and include an Approved: header with the list password in it, the message will be approved for posting to the list. The Approved: header can also appear in the first line of the body of the reply.""")), _charset=Utils.GetCharSet(lang)) dmsg['Subject'] = 'confirm ' + cookie dmsg['Sender'] = requestaddr dmsg['From'] = requestaddr dmsg['Date'] = email.utils.formatdate(localtime=True) dmsg['Message-ID'] = Utils.unique_message_id(mlist) nmsg.attach(text) nmsg.attach(message.MIMEMessage(msg)) nmsg.attach(message.MIMEMessage(dmsg)) nmsg.send(mlist, **{'tomoderators': 1}) finally: i18n.set_translation(otranslation) # Log the held message syslog('vette', '%s post from %s held, message-id=%s: %s', listname, sender, message_id, reason) # raise the specific MessageHeld exception to exit out of the message # delivery pipeline raise exc
def process(mlist, msg, msgdata): # Set the "X-Ack: no" header if noack flag is set. if msgdata.get('noack'): change_header('X-Ack', 'no', mlist, msg, msgdata) # 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. # We may have already saved it; if so, don't clobber it here. if 'original_sender' not in msgdata: 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. change_header('X-BeenThere', mlist.GetListEmail(), mlist, msg, msgdata, delete=False) # 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. change_header('X-Mailman-Version', mm_cfg.VERSION, mlist, msg, msgdata, repl=False) # We set "Precedence: list" because this is the recommendation from the # sendmail docs, the most authoritative source of this header's semantics. change_header('Precedence', 'list', mlist, msg, msgdata, repl=False) # Do we change the from so the list takes ownership of the email if (msgdata.get('from_is_list') or mlist.from_is_list) and not fasttrack: # Be as robust as possible here. faddrs = getaddresses(msg.get_all('from', [])) # Strip the nulls and bad emails. faddrs = [x for x in faddrs if x[1].find('@') > 0] if len(faddrs) == 1: realname, email = o_from = faddrs[0] else: # No From: or multiple addresses. Just punt and take # the get_sender result. realname = '' email = msgdata['original_sender'] o_from = (realname, email) if not realname: if mlist.isMember(email): realname = mlist.getMemberName(email) or email else: realname = email # Remove domain from realname if it looks like an email address realname = re.sub(r'@([^ .]+\.)+[^ .]+$', '---', realname) # Make a display name and RFC 2047 encode it if necessary. This is # difficult and kludgy. If the realname came from From: it should be # ascii or RFC 2047 encoded. If it came from the list, it should be # in the charset of the list's preferred language or possibly unicode. # if it's from the email address, it should be ascii. In any case, # make it a unicode. if isinstance(realname, unicode): urn = realname else: rn, cs = ch_oneline(realname) urn = unicode(rn, cs, errors='replace') # likewise, the list's real_name which should be ascii, but use the # charset of the list's preferred_language which should be a superset. lcs = Utils.GetCharSet(mlist.preferred_language) ulrn = unicode(mlist.real_name, lcs, errors='replace') # get translated 'via' with dummy replacements realname = '%(realname)s' lrn = '%(lrn)s' # We want the i18n context to be the list's preferred_language. It # could be the poster's. otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) via = _('%(realname)s via %(lrn)s') i18n.set_translation(otrans) uvia = unicode(via, lcs, errors='replace') # Replace the dummy replacements. uvia = re.sub(u'%\(lrn\)s', ulrn, re.sub(u'%\(realname\)s', urn, uvia)) # And get an RFC 2047 encoded header string. dn = str(Header(uvia, lcs)) change_header('From', formataddr((dn, mlist.GetListEmail())), mlist, msg, msgdata) else: # Use this as a flag o_from = None # 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?) # MAS: We need to do some things with the original From: if we've munged # it for DMARC mitigation. We have goals for this process which are # not completely compatible, so we do the best we can. Our goals are: # 1) as long as the list is not anonymous, the original From: address # should be obviously exposed, i.e. not just in a header that MUAs # don't display. # 2) the original From: address should not be in a comment or display # name in the new From: because it is claimed that multiple domains # in any fields in From: are indicative of spamminess. This means # it should be in Reply-To: or Cc:. # 3) the behavior of an MUA doing a 'reply' or 'reply all' should be # consistent regardless of whether or not the From: is munged. # Goal 3) implies sometimes the original From: should be in Reply-To: # and sometimes in Cc:, and even so, this goal won't be achieved in # all cases with all MUAs. In cases of conflict, the above ordering of # goals is priority order. 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. o_rt = False if not mlist.first_strip_reply_to: orig = msg.get_all('reply-to', []) for pair in getaddresses(orig): # There's an original Reply-To: and we're not removing it. add(pair) o_rt = True # We also need to put the old From: in Reply-To: in all cases where # it is not going in Cc:. This is when reply_goes_to_list == 0 and # either there was no original Reply-To: or we stripped it. # However, if there was an original Reply-To:, unstripped, and it # contained the original From: address we need to flag that it's # there so we don't add the original From: to Cc: if o_from and mlist.reply_goes_to_list == 0: if o_rt: if d.has_key(o_from[1].lower()): # Original From: address is in original Reply-To:. # Pretend we added it. o_from = None else: add(o_from) # Flag that we added it. o_from = None # 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') add((str(i18ndesc), mlist.GetListEmail())) # Don't put Reply-To: back if there's nothing to add! if new: # Preserve order change_header('Reply-To', COMMASPACE.join([formataddr(pair) for pair in new]), mlist, msg, msgdata) else: del msg['reply-to'] # 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. # We do add the Cc in cases where From: header munging is being done # because even though the list address is in From:, the Reply-To: # poster will override it. Brain dead MUAs may then address the list # twice on a 'reply all', but reasonable MUAs should do the right # thing. We also add the original From: to Cc: if it wasn't added # to Reply-To: add_list = (mlist.personalize == 2 and mlist.reply_goes_to_list <> 1 and not mlist.anonymous_list) if add_list or o_from: # 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 = {} # If we're adding the original From:, add it first. if o_from: add(o_from) # AvoidDuplicates may have set a new Cc: in msgdata.add_header, # so check that. if (msgdata.has_key('add_header') and msgdata['add_header'].has_key('Cc')): for pair in getaddresses([msgdata['add_header']['Cc']]): add(pair) else: for pair in getaddresses(msg.get_all('cc', [])): add(pair) if add_list: i18ndesc = uheader(mlist, mlist.description, 'Cc') add((str(i18ndesc), mlist.GetListEmail())) change_header('Cc', COMMASPACE.join([formataddr(pair) for pair in new]), mlist, msg, msgdata) # 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. change_header('List-Id', listid_h, mlist, msg, msgdata) # 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() 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(): # 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(', ')) change_header(h, v, mlist, msg, msgdata)
def __handlepost(self, record, value, comment, preserve, forward, addr): # For backwards compatibility with pre 2.0beta3 ptime, sender, subject, reason, filename, msgdata = record path = os.path.join(mm_cfg.DATA_DIR, filename) # Handle message preservation if preserve: parts = os.path.split(path)[1].split(DASH) parts[0] = 'spam' spamfile = DASH.join(parts) # Preserve the message as plain text, not as a pickle try: fp = open(path) except IOError as e: if e.errno != errno.ENOENT: raise return LOST try: if path.endswith('.pck'): msg = pickle.load(fp) else: assert path.endswith('.txt'), '%s not .pck or .txt' % path msg = fp.read() finally: fp.close() # Save the plain text to a .msg file, not a .pck file outpath = os.path.join(mm_cfg.SPAM_DIR, spamfile) head, ext = os.path.splitext(outpath) outpath = head + '.msg' outfp = open(outpath, 'wb') try: if path.endswith('.pck'): g = Generator(outfp) g.flatten(msg, 1) else: outfp.write(msg) finally: outfp.close() # Now handle updates to the database rejection = None fp = None msg = None status = REMOVE if value == mm_cfg.DEFER: # Defer status = DEFER elif value == mm_cfg.APPROVE: # Approved. try: msg = readMessage(path) except IOError as e: if e.errno != errno.ENOENT: raise return LOST msg = readMessage(path) msgdata['approved'] = 1 # adminapproved is used by the Emergency handler msgdata['adminapproved'] = 1 # Calculate a new filebase for the approved message, otherwise # delivery errors will cause duplicates. try: del msgdata['filebase'] except KeyError: pass # Queue the file for delivery by qrunner. Trying to deliver the # message directly here can lead to a huge delay in web # turnaround. Log the moderation and add a header. msg['X-Mailman-Approved-At'] = email.utils.formatdate(localtime=1) syslog('vette', '%s: held message approved, message-id: %s', self.internal_name(), msg.get('message-id', 'n/a')) # Stick the message back in the incoming queue for further # processing. inq = get_switchboard(mm_cfg.INQUEUE_DIR) inq.enqueue(msg, _metadata=msgdata) elif value == mm_cfg.REJECT: # Rejected rejection = 'Refused' lang = self.getMemberLanguage(sender) subject = Utils.oneline(subject, Utils.GetCharSet(lang)) self.__refuse(_('Posting of your message titled "%(subject)s"'), sender, comment or _('[No reason given]'), lang=lang) else: assert value == mm_cfg.DISCARD # Discarded rejection = 'Discarded' # Forward the message if forward and addr: # If we've approved the message, we need to be sure to craft a # completely unique second message for the forwarding operation, # since we don't want to share any state or information with the # normal delivery. try: copy = readMessage(path) except IOError as e: if e.errno != errno.ENOENT: raise raise Errors.LostHeldMessage(path) # It's possible the addr is a comma separated list of addresses. addrs = getaddresses([addr]) if len(addrs) == 1: realname, addr = addrs[0] # If the address getting the forwarded message is a member of # the list, we want the headers of the outer message to be # encoded in their language. Otherwise it'll be the preferred # language of the mailing list. lang = self.getMemberLanguage(addr) else: # Throw away the realnames addr = [a for realname, a in addrs] # Which member language do we attempt to use? We could use # the first match or the first address, but in the face of # ambiguity, let's just use the list's preferred language lang = self.preferred_language otrans = i18n.get_translation() i18n.set_language(lang) try: fmsg = Message.UserNotification( addr, self.GetBouncesEmail(), _('Forward of moderated message'), lang=lang) finally: i18n.set_translation(otrans) fmsg.set_type('message/rfc822') fmsg.attach(copy) fmsg.send(self) # Log the rejection if rejection: note = '''%(listname)s: %(rejection)s posting: tFrom: %(sender)s tSubject: %(subject)s''' % { 'listname': self.internal_name(), 'rejection': rejection, 'sender': str(sender).replace('%', '%%'), 'subject': str(subject).replace('%', '%%'), } if comment: note += '\n\tReason: ' + comment.replace('%', '%%') syslog('vette', note) # Always unlink the file containing the message text. It's not # necessary anymore, regardless of the disposition of the message. if status != DEFER: try: os.unlink(path) except OSError as e: if e.errno != errno.ENOENT: raise # We lost the message text file. Clean up our housekeeping # and inform of this status. return LOST return status
def process(mlist, msg, msgdata): # Set the "X-Ack: no" header if noack flag is set. if msgdata.get('noack'): change_header('X-Ack', 'no', mlist, msg, msgdata) # 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. # We may have already saved it; if so, don't clobber it here. if 'original_sender' not in msgdata: 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. change_header('X-BeenThere', mlist.GetListEmail(), mlist, msg, msgdata, delete=False) # 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. change_header('X-Mailman-Version', mm_cfg.VERSION, mlist, msg, msgdata, repl=False) # We set "Precedence: list" because this is the recommendation from the # sendmail docs, the most authoritative source of this header's semantics. change_header('Precedence', 'list', mlist, msg, msgdata, repl=False) # Do we change the from so the list takes ownership of the email if (msgdata.get('from_is_list') or mlist.from_is_list) and not fasttrack: # Be as robust as possible here. faddrs = getaddresses(msg.get_all('from', [])) # Strip the nulls and bad emails. faddrs = [x for x in faddrs if x[1].find('@') > 0] if len(faddrs) == 1: realname, email = o_from = faddrs[0] else: # No From: or multiple addresses. Just punt and take # the get_sender result. realname = '' email = msgdata['original_sender'] o_from = (realname, email) if not realname: if mlist.isMember(email): realname = mlist.getMemberName(email) or email else: realname = email # Remove domain from realname if it looks like an email address realname = re.sub(r'@([^ .]+\.)+[^ .]+$', '---', realname) # Make a display name and RFC 2047 encode it if necessary. This is # difficult and kludgy. If the realname came from From: it should be # ascii or RFC 2047 encoded. If it came from the list, it should be # in the charset of the list's preferred language or possibly unicode. # if it's from the email address, it should be ascii. In any case, # make it a unicode. if isinstance(realname, unicode): urn = realname else: rn, cs = ch_oneline(realname) urn = unicode(rn, cs, errors='replace') # likewise, the list's real_name which should be ascii, but use the # charset of the list's preferred_language which should be a superset. lcs = Utils.GetCharSet(mlist.preferred_language) ulrn = unicode(mlist.real_name, lcs, errors='replace') # get translated 'via' with dummy replacements realname = '%(realname)s' lrn = '%(lrn)s' # We want the i18n context to be the list's preferred_language. It # could be the poster's. otrans = i18n.get_translation() i18n.set_language(mlist.preferred_language) via = _('%(realname)s via %(lrn)s') i18n.set_translation(otrans) uvia = unicode(via, lcs, errors='replace') # Replace the dummy replacements. uvia = re.sub(u'%\(lrn\)s', ulrn, re.sub(u'%\(realname\)s', urn, uvia)) # And get an RFC 2047 encoded header string. dn = str(Header(uvia, lcs)) change_header('From', formataddr((dn, mlist.GetListEmail())), mlist, msg, msgdata) else: # Use this as a flag o_from = None # 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?) # MAS: We need to do some things with the original From: if we've munged # it for DMARC mitigation. We have goals for this process which are # not completely compatible, so we do the best we can. Our goals are: # 1) as long as the list is not anonymous, the original From: address # should be obviously exposed, i.e. not just in a header that MUAs # don't display. # 2) the original From: address should not be in a comment or display # name in the new From: because it is claimed that multiple domains # in any fields in From: are indicative of spamminess. This means # it should be in Reply-To: or Cc:. # 3) the behavior of an MUA doing a 'reply' or 'reply all' should be # consistent regardless of whether or not the From: is munged. # Goal 3) implies sometimes the original From: should be in Reply-To: # and sometimes in Cc:, and even so, this goal won't be achieved in # all cases with all MUAs. In cases of conflict, the above ordering of # goals is priority order. 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. o_rt = False if not mlist.first_strip_reply_to: orig = msg.get_all('reply-to', []) for pair in getaddresses(orig): # There's an original Reply-To: and we're not removing it. add(pair) o_rt = True # We also need to put the old From: in Reply-To: in all cases where # it is not going in Cc:. This is when reply_goes_to_list == 0 and # either there was no original Reply-To: or we stripped it. # However, if there was an original Reply-To:, unstripped, and it # contained the original From: address we need to flag that it's # there so we don't add the original From: to Cc: if o_from and mlist.reply_goes_to_list == 0: if o_rt: if d.has_key(o_from[1].lower()): # Original From: address is in original Reply-To:. # Pretend we added it. o_from = None else: add(o_from) # Flag that we added it. o_from = None # 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') add((str(i18ndesc), mlist.GetListEmail())) # Don't put Reply-To: back if there's nothing to add! if new: # Preserve order change_header('Reply-To', COMMASPACE.join([formataddr(pair) for pair in new]), mlist, msg, msgdata) else: del msg['reply-to'] # 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. # We do add the Cc in cases where From: header munging is being done # because even though the list address is in From:, the Reply-To: # poster will override it. Brain dead MUAs may then address the list # twice on a 'reply all', but reasonable MUAs should do the right # thing. We also add the original From: to Cc: if it wasn't added # to Reply-To: add_list = (mlist.personalize == 2 and mlist.reply_goes_to_list <> 1 and not mlist.anonymous_list) if add_list or o_from: # 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 = {} # If we're adding the original From:, add it first. if o_from: add(o_from) # AvoidDuplicates may have set a new Cc: in msgdata.add_header, # so check that. if (msgdata.has_key('add_header') and msgdata['add_header'].has_key('Cc')): for pair in getaddresses([msgdata['add_header']['Cc']]): add(pair) else: for pair in getaddresses(msg.get_all('cc', [])): add(pair) if add_list: i18ndesc = uheader(mlist, mlist.description, 'Cc') add((str(i18ndesc), mlist.GetListEmail())) change_header('Cc', COMMASPACE.join([formataddr(pair) for pair in new]), mlist, msg, msgdata) # 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. change_header('List-Id', listid_h, mlist, msg, msgdata) # 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() 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(): # 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(', ')) change_header(h, v, mlist, msg, msgdata)
def __init__(self, message=None, sequence=0, keepHeaders=[], lang=mm_cfg.DEFAULT_SERVER_LANGUAGE, mlist=None): self.__super_init(message, sequence, keepHeaders) self.prev = None self.next = None # Trim Re: from the subject line i = 0 while i != -1: result = REpat.match(self.subject) if result: i = result.end(0) self.subject = self.subject[i:] else: i = -1 # Useful to keep around self._lang = lang self._mlist = mlist if mm_cfg.ARCHIVER_OBSCURES_EMAILADDRS: # Avoid i18n side-effects. Note that the language for this # article (for this list) could be different from the site-wide # preferred language, so we need to ensure no side-effects will # occur. Think what happens when executing bin/arch. otrans = i18n.get_translation() try: i18n.set_language(lang) if self.author == self.email: self.author = self.email = re.sub('@', _(' at '), self.email) else: self.email = re.sub('@', _(' at '), self.email) finally: i18n.set_translation(otrans) # Snag the content-* headers. RFC 1521 states that their values are # case insensitive. ctype = message.get('Content-Type', 'text/plain') cenc = message.get('Content-Transfer-Encoding', '') self.ctype = ctype.lower() self.cenc = cenc.lower() self.decoded = {} cset = Utils.GetCharSet(mlist.preferred_language) cset_out = Charset(cset).output_charset or cset if isinstance(cset_out, unicode): # email 3.0.1 (python 2.4) doesn't like unicode cset_out = cset_out.encode('us-ascii') charset = message.get_content_charset(cset_out) if charset: charset = charset.lower().strip() if charset[0] == '"' and charset[-1] == '"': charset = charset[1:-1] if charset[0] == "'" and charset[-1] == "'": charset = charset[1:-1] try: body = message.get_payload(decode=True) except binascii.Error: body = None if body and charset != Utils.GetCharSet(self._lang): # decode body try: body = unicode(body, charset) except (UnicodeError, LookupError): body = None if body: self.body = [l + "\n" for l in body.splitlines()] self.decode_headers()
def sizeof(filename, lang): try: size = os.path.getsize(filename) except OSError, e: # ENOENT can happen if the .mbox file was moved away or deleted, and # an explicit mbox file name was given to bin/arch. if e.errno <> errno.ENOENT: raise return _('size not available') if size < 1000: # Avoid i18n side-effects otrans = i18n.get_translation() try: i18n.set_language(lang) out = _(' %(size)i bytes ') finally: i18n.set_translation(otrans) return out elif size < 1000000: return ' %d KB ' % (size / 1000) # GB?? :-) return ' %d MB ' % (size / 1000000) html_charset = '<META http-equiv="Content-Type" ' \ 'content="text/html; charset=%s">' def CGIescape(arg, lang=None): if isinstance(arg, types.UnicodeType): s = Utils.websafe(arg) else:
def hold_for_approval(mlist, msg, msgdata, exc): # BAW: This should really be tied into the email confirmation system so # that the message can be approved or denied via email as well as the # web. # # XXX We use the weird type(type) construct below because in Python 2.1, # type is a function not a type and so can't be used as the second # argument in isinstance(). However, in Python 2.5, exceptions are # new-style classes and so are not of ClassType. if isinstance(exc, ClassType) or isinstance(exc, type(type)): # Go ahead and instantiate it now. exc = exc() listname = mlist.real_name sender = msgdata.get('sender', msg.get_sender()) usersubject = msg.get('subject') charset = Utils.GetCharSet(mlist.preferred_language) if usersubject: usersubject = Utils.oneline(usersubject, charset) else: usersubject = _('(no subject)') message_id = msg.get('message-id', 'n/a') owneraddr = mlist.GetOwnerEmail() adminaddr = mlist.GetBouncesEmail() requestaddr = mlist.GetRequestEmail() # We need to send both the reason and the rejection notice through the # translator again, because of the games we play above reason = Utils.wrap(exc.reason_notice()) msgdata['rejection_notice'] = Utils.wrap(exc.rejection_notice(mlist)) id = mlist.HoldMessage(msg, reason, msgdata) # Now we need to craft and send a message to the list admin so they can # deal with the held message. d = {'listname' : listname, 'hostname' : mlist.host_name, 'reason' : _(reason), 'sender' : sender, 'subject' : usersubject, 'admindb_url': mlist.GetScriptURL('admindb', absolute=1), } # We may want to send a notification to the original sender too fromusenet = msgdata.get('fromusenet') # Since we're sending two messages, which may potentially be in different # languages (the user's preferred and the list's preferred for the admin), # we need to play some i18n games here. Since the current language # context ought to be set up for the user, let's craft his message first. cookie = mlist.pend_new(Pending.HELD_MESSAGE, id) if not fromusenet and ackp(msg) and mlist.respond_to_post_requests and \ mlist.autorespondToSender(sender, mlist.getMemberLanguage(sender)): # Get a confirmation cookie d['confirmurl'] = '%s/%s' % (mlist.GetScriptURL('confirm', absolute=1), cookie) lang = msgdata.get('lang', mlist.getMemberLanguage(sender)) subject = _('Your message to %(listname)s awaits moderator approval') text = Utils.maketext('postheld.txt', d, lang=lang, mlist=mlist) nmsg = Message.UserNotification(sender, owneraddr, subject, text, lang) nmsg.send(mlist) # Now the message for the list owners. Be sure to include the list # moderators in this message. This one should appear to come from # <list>-owner since we really don't need to do bounce processing on it. if mlist.admin_immed_notify: # Now let's temporarily set the language context to that which the # admin is expecting. otranslation = i18n.get_translation() i18n.set_language(mlist.preferred_language) try: lang = mlist.preferred_language charset = Utils.GetCharSet(lang) # We need to regenerate or re-translate a few values in d d['reason'] = _(reason) d['subject'] = usersubject # craft the admin notification message and deliver it subject = _('%(listname)s post from %(sender)s requires approval') nmsg = Message.UserNotification(owneraddr, owneraddr, subject, lang=lang) nmsg.set_type('multipart/mixed') text = MIMEText( Utils.maketext('postauth.txt', d, raw=1, mlist=mlist), _charset=charset) dmsg = MIMEText(Utils.wrap(_("""\ If you reply to this message, keeping the Subject: header intact, Mailman will discard the held message. Do this if the message is spam. If you reply to this message and include an Approved: header with the list password in it, the message will be approved for posting to the list. The Approved: header can also appear in the first line of the body of the reply.""")), _charset=Utils.GetCharSet(lang)) dmsg['Subject'] = 'confirm ' + cookie dmsg['Sender'] = requestaddr dmsg['From'] = requestaddr dmsg['Date'] = email.Utils.formatdate(localtime=True) dmsg['Message-ID'] = Utils.unique_message_id(mlist) nmsg.attach(text) nmsg.attach(MIMEMessage(msg)) nmsg.attach(MIMEMessage(dmsg)) nmsg.send(mlist, **{'tomoderators': 1}) finally: i18n.set_translation(otranslation) # Log the held message syslog('vette', '%s post from %s held, message-id=%s: %s', listname, sender, message_id, reason) # raise the specific MessageHeld exception to exit out of the message # delivery pipeline raise exc
_("""\ An attempt was made to subscribe your address to the mailing list %(listaddr)s. You are already subscribed to this mailing list. Note that the list membership is not public, so it is possible that a bad person was trying to probe the list for its membership. This would be a privacy violation if we let them do this, but we didn't. If you submitted the subscription request and forgot that you were already subscribed to the list, then you can ignore this message. If you suspect that an attempt is being made to covertly discover whether you are a member of this list, and you are worried about your privacy, then feel free to send a message to the list administrator at %(listowner)s. """), lang=mlang) finally: i18n.set_translation(otrans) msg.send(mlist) # These shouldn't happen unless someone's tampering with the form except Errors.MMCantDigestError: results = _('This list does not support digest delivery.') except Errors.MMMustDigestError: results = _('This list only supports digest delivery.') else: # Everything's cool. Our return string actually depends on whether # this list has private rosters or not if privacy_results: results = privacy_results else: results = _("""\ You have been successfully subscribed to the %(realname)s mailing list.""") # Show the results
def process_form(mlist, doc, cgidata, lang): listowner = mlist.GetOwnerEmail() realname = mlist.real_name results = [] # The email address being subscribed, required email = cgidata.getfirst('email', '').strip() if not email: results.append(_('You must supply a valid email address.')) fullname = cgidata.getfirst('fullname', '') # Canonicalize the full name fullname = Utils.canonstr(fullname, lang) # Who was doing the subscribing? remote = os.environ.get( 'HTTP_FORWARDED_FOR', os.environ.get('HTTP_X_FORWARDED_FOR', os.environ.get('REMOTE_ADDR', 'unidentified origin'))) # Check reCAPTCHA submission, if enabled if mm_cfg.RECAPTCHA_SECRET_KEY: request = urllib.request.Request( url='https://www.google.com/recaptcha/api/siteverify', data=urllib.parse.urlencode({ 'secret': mm_cfg.RECAPTCHA_SECRET_KEY, 'response': cgidata.getvalue('g-recaptcha-response', ''), 'remoteip': remote })) try: httpresp = urllib.request.urlopen(request) captcha_response = json.load(httpresp) httpresp.close() if not captcha_response['success']: e_codes = COMMASPACE.join(captcha_response['error-codes']) results.append(_('reCAPTCHA validation failed: %(e_codes)s')) except urllib.error.URLError as e: e_reason = e.reason results.append(_('reCAPTCHA could not be validated: %(e_reason)s')) # Are we checking the hidden data? if mm_cfg.SUBSCRIBE_FORM_SECRET: now = int(time.time()) # Try to accept a range in case of load balancers, etc. (LP: #1447445) if remote.find('.') >= 0: # ipv4 - drop last octet remote1 = remote.rsplit('.', 1)[0] else: # ipv6 - drop last 16 (could end with :: in which case we just # drop one : resulting in an invalid format, but it's only # for our hash so it doesn't matter. remote1 = remote.rsplit(':', 1)[0] try: ftime, fhash = cgidata.getfirst('sub_form_token', '').split(':') then = int(ftime) except ValueError: ftime = fhash = '' then = 0 token = Utils.sha_new( (mm_cfg.SUBSCRIBE_FORM_SECRET + ":" + ftime + ":" + mlist.internal_name() + ":" + remote1).encode()).hexdigest() if ftime and now - then > mm_cfg.FORM_LIFETIME: results.append(_('The form is too old. Please GET it again.')) if ftime and now - then < mm_cfg.SUBSCRIBE_FORM_MIN_TIME: results.append( _('Please take a few seconds to fill out the form before submitting it.' )) if ftime and token != fhash: results.append( _("The hidden token didn't match. Did your IP change?")) if not ftime: results.append( _('There was no hidden token in your submission or it was corrupted.' )) results.append(_('You must GET the form before submitting it.')) # Was an attempt made to subscribe the list to itself? if email == mlist.GetListEmail(): syslog('mischief', 'Attempt to self subscribe %s: %s', email, remote) results.append(_('You may not subscribe a list to itself!')) # If the user did not supply a password, generate one for him password = cgidata.getfirst('pw', '').strip() confirmed = cgidata.getfirst('pw-conf', '').strip() if not password and not confirmed: password = Utils.MakeRandomPassword() elif not password or not confirmed: results.append(_('If you supply a password, you must confirm it.')) elif password != confirmed: results.append(_('Your passwords did not match.')) # Get the digest option for the subscription. digestflag = cgidata.getfirst('digest') if digestflag: try: digest = int(digestflag) except (TypeError, ValueError): digest = 0 else: digest = mlist.digest_is_default # Sanity check based on list configuration. BAW: It's actually bogus that # the page allows you to set the digest flag if you don't really get the # choice. :/ if not mlist.digestable: digest = 0 elif not mlist.nondigestable: digest = 1 if results: print_results(mlist, ERRORSEP.join(results), doc, lang) return # If this list has private rosters, we have to be careful about the # message that gets printed, otherwise the subscription process can be # used to mine for list members. It may be inefficient, but it's still # possible, and that kind of defeats the purpose of private rosters. # We'll use this string for all successful or unsuccessful subscription # results. if mlist.private_roster == 0: # Public rosters privacy_results = '' else: privacy_results = _("""\ Your subscription request has been received, and will soon be acted upon. Depending on the configuration of this mailing list, your subscription request may have to be first confirmed by you via email, or approved by the list moderator. If confirmation is required, you will soon get a confirmation email which contains further instructions.""") try: userdesc = UserDesc(email, fullname, password, digest, lang) mlist.AddMember(userdesc, remote) results = '' # Check for all the errors that mlist.AddMember can throw options on the # web page for this cgi except Errors.MembershipIsBanned: results = _("""The email address you supplied is banned from this mailing list. If you think this restriction is erroneous, please contact the list owners at %(listowner)s.""") except Errors.MMBadEmailError: results = _("""\ The email address you supplied is not valid. (E.g. it must contain an `@'.)""") except Errors.MMHostileAddress: results = _("""\ Your subscription is not allowed because the email address you gave is insecure.""") except Errors.MMSubscribeNeedsConfirmation: # Results string depends on whether we have private rosters or not if privacy_results: results = privacy_results else: results = _("""\ Confirmation from your email address is required, to prevent anyone from subscribing you without permission. Instructions are being sent to you at %(email)s. Please note your subscription will not start until you confirm your subscription.""") except Errors.MMNeedApproval as x: # Results string depends on whether we have private rosters or not if privacy_results: results = privacy_results else: # We need to interpolate into x.__str__() x = _(str(x)) results = _("""\ Your subscription request was deferred because %(x)s. Your request has been forwarded to the list moderator. You will receive email informing you of the moderator's decision when they get to your request.""") except Errors.MMAlreadyAMember: # Results string depends on whether we have private rosters or not if not privacy_results: results = _('You are already subscribed.') else: results = privacy_results # This could be a membership probe. For safety, let the user know # a probe occurred. BAW: should we inform the list moderator? listaddr = mlist.GetListEmail() # Set the language for this email message to the member's language. mlang = mlist.getMemberLanguage(email) otrans = i18n.get_translation() i18n.set_language(mlang) try: msg = Message.UserNotification(mlist.getMemberCPAddress(email), mlist.GetBouncesEmail(), _('Mailman privacy alert'), _("""\ An attempt was made to subscribe your address to the mailing list %(listaddr)s. You are already subscribed to this mailing list. Note that the list membership is not public, so it is possible that a bad person was trying to probe the list for its membership. This would be a privacy violation if we let them do this, but we didn't. If you submitted the subscription request and forgot that you were already subscribed to the list, then you can ignore this message. If you suspect that an attempt is being made to covertly discover whether you are a member of this list, and you are worried about your privacy, then feel free to send a message to the list administrator at %(listowner)s. """), lang=mlang) finally: i18n.set_translation(otrans) msg.send(mlist) # These shouldn't happen unless someone's tampering with the form except Errors.MMCantDigestError: results = _('This list does not support digest delivery.') except Errors.MMMustDigestError: results = _('This list only supports digest delivery.') else: # Everything's cool. Our return string actually depends on whether # this list has private rosters or not if privacy_results: results = privacy_results else: results = _("""\ You have been successfully subscribed to the %(realname)s mailing list.""") # Show the results print_results(mlist, results, doc, lang)