def loginpage(mlist, scriptname, msg='', frontpage=None): url = mlist.GetScriptURL(scriptname) if frontpage: actionurl = url else: actionurl = Utils.GetRequestURI(url) if msg: msg = FontAttr(msg, color='#ff0000', size='+1').Format() # give an HTTP 401 for authentication failure print 'Status: 401 Unauthorized' if scriptname == 'admindb': who = _('Moderator') else: who = _('Administrator') # Language stuff charset = Utils.GetCharSet(mlist.preferred_language) print 'Content-type: text/html; charset=' + charset + '\n\n' print Utils.maketext( 'admlogin.html', {'listname': mlist.real_name, 'path' : actionurl, 'message' : msg, 'who' : who, }, mlist=mlist) print mlist.GetMailmanFooter()
def loginpage(mlist, scriptname, msg='', frontpage=None): url = mlist.GetScriptURL(scriptname) if frontpage: actionurl = url else: actionurl = Utils.GetRequestURI(url) if msg: msg = FontAttr(msg, color='#ff0000', size='+1').Format() # give an HTTP 401 for authentication failure print 'Status: 401 Unauthorized' if scriptname == 'admindb': who = _('Moderator') else: who = _('Administrator') # Language stuff charset = Utils.GetCharSet(mlist.preferred_language) print 'Content-type: text/html; charset=' + charset + '\n\n' print Utils.maketext('admlogin.html', { 'listname': mlist.real_name, 'path': actionurl, 'message': msg, 'who': who, }, mlist=mlist) print mlist.GetMailmanFooter()
def HoldUnsubscription(self, addr): # Assure the database is open for writing self.__opendb() # Get the next unique id id = self.__nextid() # All we need to do is save the unsubscribing address self.__db[id] = (UNSUBSCRIPTION, addr) syslog('vette', '%s: held unsubscription request from %s', self.internal_name(), addr) # Possibly notify the administrator of the hold if self.admin_immed_notify: realname = self.real_name subject = _( 'New unsubscription request from %(realname)s by %(addr)s') text = Utils.maketext( 'unsubauth.txt', {'username' : addr, ## cpanel patch 'listname' : self.real_name, 'hostname' : self.host_name, 'admindb_url': self.GetScriptURL('admindb', absolute=1), }, mlist=self) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. owneraddr = self.GetOwnerEmail() msg = Message.UserNotification(owneraddr, owneraddr, subject, text, self.preferred_language) msg.send(self, **{'tomoderators': 1})
def __sendAdminBounceNotice(self, member, msg, did=_('disabled')): # BAW: This is a bit kludgey, but we're not providing as much # information in the new admin bounce notices as we used to (some of # it was of dubious value). However, we'll provide empty, strange, or # meaningless strings for the unused %()s fields so that the language # translators don't have to provide new templates. siteowner = Utils.get_site_email(self.host_name) text = Utils.maketext('bounce.txt', { 'listname': self.real_name, 'addr': member, 'negative': '', 'did': did, 'but': '', 'reenable': '', 'owneraddr': siteowner, }, mlist=self) subject = _('Bounce action notification') umsg = Message.UserNotification(self.GetOwnerEmail(), siteowner, subject, lang=self.preferred_language) # BAW: Be sure you set the type before trying to attach, or you'll get # a MultipartConversionError. umsg.set_type('multipart/mixed') umsg.attach( MIMEText(text, _charset=Utils.GetCharSet(self.preferred_language))) if isinstance(msg, StringType): umsg.attach(MIMEText(msg)) else: umsg.attach(MIMEMessage(msg)) umsg.send(self)
def ParseTags(self, template, replacements, lang=None): if lang is None: charset = 'us-ascii' else: charset = Utils.GetCharSet(lang) text = Utils.maketext(template, raw=1, lang=lang, mlist=self) parts = re.split('(</?[Mm][Mm]-[^>]*>)', text) i = 1 while i < len(parts): tag = parts[i].lower() if replacements.has_key(tag): repl = replacements[tag] if isinstance(repl, type(u'')): repl = repl.encode(charset, 'replace') parts[i] = repl else: parts[i] = '' # EGL: Munged these values to handle the separate hostnames for the # mail and web servers. if tag == "<mm-posting-addr>": parts[i] = parts[i].replace("mail.", "") i = i + 2 return EMPTYSTRING.join(parts)
def process(mlist, msg, msgdata): # Extract the sender's address and find them in the user database sender = msgdata.get('original_sender', msg.get_sender()) try: ack = mlist.getMemberOption(sender, mm_cfg.AcknowledgePosts) if not ack: return except Errors.NotAMemberError: return # Okay, they want acknowledgement of their post. Give them their original # subject. BAW: do we want to use the decoded header? origsubj = msgdata.get('origsubj', msg.get('subject', _('(no subject)'))) # Get the user's preferred language lang = msgdata.get('lang', mlist.getMemberLanguage(sender)) # Now get the acknowledgement template realname = mlist.real_name text = Utils.maketext( 'postack.txt', {'subject' : Utils.oneline(origsubj, Utils.GetCharSet(lang)), 'listname' : realname, 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), 'optionsurl' : mlist.GetOptionsURL(sender, absolute=1), }, lang=lang, mlist=mlist, raw=1) # Craft the outgoing message, with all headers and attributes # necessary for general delivery. Then enqueue it to the outgoing # queue. subject = _('%(realname)s post acknowledgement') usermsg = Message.UserNotification(sender, mlist.GetBouncesEmail(), subject, text, lang) usermsg.send(mlist)
def process(mlist, msg, msgdata): # Extract the sender's address and find them in the user database sender = msgdata.get('original_sender', msg.get_sender()) try: ack = mlist.getMemberOption(sender, mm_cfg.AcknowledgePosts) if not ack: return except Errors.NotAMemberError: return # Okay, they want acknowledgement of their post. Give them their original # subject. BAW: do we want to use the decoded header? origsubj = msgdata.get('origsubj', msg.get('subject', _('(no subject)'))) # Get the user's preferred language lang = msgdata.get('lang', mlist.getMemberLanguage(sender)) # Now get the acknowledgement template realname = mlist.real_name text = Utils.maketext( 'postack.txt', { 'subject': Utils.oneline(origsubj, Utils.GetCharSet(lang)), 'listname': realname, 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), 'optionsurl': mlist.GetOptionsURL(sender, absolute=1), }, lang=lang, mlist=mlist, raw=1) # Craft the outgoing message, with all headers and attributes # necessary for general delivery. Then enqueue it to the outgoing # queue. subject = _('%(realname)s post acknowledgement') usermsg = Message.UserNotification(sender, mlist.GetBouncesEmail(), subject, text, lang) usermsg.send(mlist)
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 __sendAdminBounceNotice(self, member, msg): # BAW: This is a bit kludgey, but we're not providing as much # information in the new admin bounce notices as we used to (some of # it was of dubious value). However, we'll provide empty, strange, or # meaningless strings for the unused %()s fields so that the language # translators don't have to provide new templates. siteowner = Utils.get_site_email(self.host_name) text = Utils.maketext( "bounce.txt", { "listname": self.real_name, "addr": member, "negative": "", "did": _("disabled"), "but": "", "reenable": "", "owneraddr": siteowner, }, mlist=self, ) subject = _("Bounce action notification") umsg = Message.UserNotification(self.GetOwnerEmail(), siteowner, subject, lang=self.preferred_language) # BAW: Be sure you set the type before trying to attach, or you'll get # a MultipartConversionError. umsg.set_type("multipart/mixed") umsg.attach(MIMEText(text, _charset=Utils.GetCharSet(self.preferred_language))) if isinstance(msg, StringType): umsg.attach(MIMEText(msg)) else: umsg.attach(MIMEMessage(msg)) umsg.send(self)
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 __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 HoldUnsubscription(self, addr): # Assure the database is open for writing self.__opendb() # Get the next unique id id = self.__nextid() # All we need to do is save the unsubscribing address self.__db[id] = (UNSUBSCRIPTION, addr) syslog('vette', '%s: held unsubscription request from %s', self.internal_name(), addr) # Possibly notify the administrator of the hold if self.admin_immed_notify: realname = self.real_name subject = _( 'New unsubscription request from %(realname)s by %(addr)s') text = Utils.maketext( 'unsubauth.txt', { 'username': addr, 'listname': self.internal_name(), 'hostname': self.host_name, 'admindb_url': self.GetScriptURL('admindb', absolute=1), }, mlist=self) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. owneraddr = self.GetOwnerEmail() msg = Message.UserNotification(owneraddr, owneraddr, subject, text, self.preferred_language) msg.send(self, **{'tomoderators': 1})
def create(self, email): if self.exists: raise ListAlreadyExists langs = [mm_cfg.DEFAULT_SERVER_LANGUAGE] pw = Utils.MakeRandomPassword() pw_hashed = Utils.sha_new(pw).hexdigest() urlhost = mm_cfg.DEFAULT_URL_HOST host_name = mm_cfg.DEFAULT_EMAIL_HOST web_page_url = mm_cfg.DEFAULT_URL_PATTERN % urlhost # TODO: Add some atomicity. We should roll back changes using # a try/else if something (like MTA alias update) fails # before the function terminates. try: oldmask = os.umask(002) self.mlist.Create(self.name, email, pw_hashed, langs=langs, emailhost=host_name, urlhost=urlhost) self.mlist.preferred_language = langs[0] # Reply-To set to list address self.mlist.reply_goes_to_list = 2 self.mlist.reply_to_address = "%s@%s" % (self.list, self.domain) # Allow messages from listname@domain self.mlist.acceptable_aliases = "%s@%s\n" % (self.list, self.domain) self.mlist.subject_prefix = "[%s] " % (self.list) self.mlist.msg_footer = "" self.mlist.subscribe_policy = 2 # Confirm and approve self.mlist.max_message_size = 20480 # 20M self.mlist.Save() finally: os.umask(oldmask) self.mlist.Unlock() if mm_cfg.MTA: modname = 'Mailman.MTA.' + mm_cfg.MTA __import__(modname) sys.modules[modname].create(self.mlist) siteowner = Utils.get_site_email(self.mlist.host_name, 'owner') text = Utils.maketext( 'newlist.txt', {'listname' : self.name, 'password' : pw, 'admin_url' : self.mlist.GetScriptURL('admin', absolute=1), 'listinfo_url': self.mlist.GetScriptURL('listinfo', absolute=1), 'requestaddr' : self.mlist.GetRequestEmail(), 'siteowner' : siteowner, }, mlist=self.mlist) msg = Message.UserNotification(email, siteowner, 'Your new mailing list: %s' % self.name, text, self.mlist.preferred_language) msg.send(self.mlist)
def process(res, args): # Get the help text introduction mlist = res.mlist # Since this message is personalized, add some useful information if the # address requesting help is a member of the list. msg = res.msg for sender in msg.get_senders(): if mlist.isMember(sender): memberurl = mlist.GetOptionsURL(sender, absolute=1) urlhelp = _( 'You can access your personal options via the following url:') res.results.append(urlhelp) res.results.append(memberurl) # Get a blank line in the output. res.results.append('') break # build the specific command helps from the module docstrings modhelps = {} import Mailman.Commands path = os.path.dirname(os.path.abspath(Mailman.Commands.__file__)) for file in os.listdir(path): if not file.startswith('cmd_') or not file.endswith('.py'): continue module = os.path.splitext(file)[0] modname = 'Mailman.Commands.' + module try: __import__(modname) except ImportError: continue cmdname = module[4:] help = None if hasattr(sys.modules[modname], 'gethelp'): help = sys.modules[modname].gethelp(mlist) if help: modhelps[cmdname] = help # Now sort the command helps helptext = [] keys = list(modhelps.keys()) keys.sort() for cmd in keys: helptext.append(modhelps[cmd]) commands = EMPTYSTRING.join(helptext) # Now craft the response helptext = Utils.maketext( 'help.txt', { 'listname': mlist.real_name, 'version': mm_cfg.VERSION, 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), 'requestaddr': mlist.GetRequestEmail(), 'adminaddr': mlist.GetOwnerEmail(), 'commands': commands, }, mlist=mlist, lang=res.msgdata['lang'], raw=1) # Now add to the response res.results.append('help') res.results.append(helptext)
def hold_for_approval(mlist, msg, msgdata, exc): # TBD: 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. That's for later though, because it would mean a revamp of the # MailCommandHandler too. # if type(exc) is ClassType: # Go ahead and instantiate it now. exc = exc() listname = mlist.real_name reason = str(exc) sender = msg.GetSender() adminaddr = mlist.GetAdminEmail() msgdata['rejection-notice'] = exc.rejection_notice(mlist) 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' : msg.get('subject', '(no subject)'), 'admindb_url': mlist.GetScriptURL('admindb', absolute=1), } if mlist.admin_immed_notify: # get the text from the template subject = '%s post from %s requires approval' % (listname, sender) text = Utils.maketext('postauth.txt', d, raw=1) # craft the admin notification message and deliver it msg = Message.UserNotification(adminaddr, adminaddr, subject, text) HandlerAPI.DeliverToUser(mlist, msg) # We may want to send a notification to the original sender too fromusenet = msgdata.get('fromusenet') if not fromusenet and not mlist.dont_respond_to_post_requests: subject = 'Your message to %s awaits moderator approval' % listname text = Utils.maketext('postheld.txt', d) msg = Message.UserNotification(sender, adminaddr, subject, text) HandlerAPI.DeliverToUser(mlist, msg) # Log the held message syslog('vette', '%s post from %s held: %s' % (listname, sender, reason)) # raise the specific MessageHeld exception to exit out of the message # delivery pipeline raise exc
def process(res, args): # Get the help text introduction mlist = res.mlist # Since this message is personalized, add some useful information if the # address requesting help is a member of the list. msg = res.msg for sender in msg.get_senders(): if mlist.isMember(sender): memberurl = mlist.GetOptionsURL(sender, absolute=1) urlhelp = _( 'You can access your personal options via the following url:') res.results.append(urlhelp) res.results.append(memberurl) # Get a blank line in the output. res.results.append('') break # build the specific command helps from the module docstrings modhelps = {} import Mailman.Commands path = os.path.dirname(os.path.abspath(Mailman.Commands.__file__)) for file in os.listdir(path): if not file.startswith('cmd_') or not file.endswith('.py'): continue module = os.path.splitext(file)[0] modname = 'Mailman.Commands.' + module try: __import__(modname) except ImportError: continue cmdname = module[4:] help = None if hasattr(sys.modules[modname], 'gethelp'): help = sys.modules[modname].gethelp(mlist) if help: modhelps[cmdname] = help # Now sort the command helps helptext = [] keys = modhelps.keys() keys.sort() for cmd in keys: helptext.append(modhelps[cmd]) commands = EMPTYSTRING.join(helptext) # Now craft the response helptext = Utils.maketext( 'help.txt', {'listname' : mlist.real_name, 'version' : mm_cfg.VERSION, 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), 'requestaddr' : mlist.GetRequestEmail(), 'adminaddr' : mlist.GetOwnerEmail(), 'commands' : commands, }, mlist=mlist, lang=res.msgdata['lang'], raw=1) # Now add to the response res.results.append('help') res.results.append(helptext)
def SendSubscribeAck(self, name, password, digest, text=""): pluser = self.getMemberLanguage(name) # Need to set this here to get the proper l10n of the Subject: i18n.set_language(pluser) 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 = "" # get the text from the template text += Utils.maketext( "subscribeack.txt", { "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 GetConfigInfo(self): WIDTH = mm_cfg.TEXTFIELDWIDTH return [ "Batched-delivery digest characteristics.", ('digestable', mm_cfg.Toggle, ('No', 'Yes'), 1, 'Can list members choose to receive list traffic ' 'bunched in digests?'), ('digest_is_default', mm_cfg.Radio, ('Regular', 'Digest'), 0, 'Which delivery mode is the default for new users?'), ('mime_is_default_digest', mm_cfg.Radio, ('Plain', 'Mime'), 0, 'When receiving digests, which format is default?'), ('digest_size_threshhold', mm_cfg.Number, 3, 0, 'How big in Kb should a digest be before it gets sent out?'), # Should offer a 'set to 0' for no size threshhold. ('digest_send_periodic', mm_cfg.Radio, ('No', 'Yes'), 1, 'Should a digest be dispatched daily when the size threshold ' "isn't reached?"), ('digest_header', mm_cfg.Text, (4, WIDTH), 0, 'Header added to every digest', "Text attached (as an initial message, before the table" " of contents) to the top of digests. " + Utils.maketext('headfoot.html', raw=1)), ('digest_footer', mm_cfg.Text, (4, WIDTH), 0, 'Footer added to every digest', "Text attached (as a final message) to the bottom of digests. " + Utils.maketext('headfoot.html', raw=1)), ]
def MailUserPassword(self, user): listfullname = '%s@%s' % (self.real_name, self.host_name) requestaddr = self.GetRequestEmail() # find the lowercased version of the user's address adminaddr = self.GetBouncesEmail() assert self.isMember(user) if not self.getMemberPassword(user): # The user's password somehow got corrupted. Generate a new one # for him, after logging this bogosity. syslog('error', 'User %s had a false password for list %s', user, self.internal_name()) waslocked = self.Locked() if not waslocked: self.Lock() try: self.setMemberPassword(user, Utils.MakeRandomPassword()) self.Save() finally: if not waslocked: self.Unlock() # Now send the user his password cpuser = self.getMemberCPAddress(user) recipient = self.GetMemberAdminEmail(cpuser) subject = _('%(listfullname)s mailing list reminder') # Get user's language and charset lang = self.getMemberLanguage(user) cset = Utils.GetCharSet(lang) password = self.getMemberPassword(user) # TK: Make unprintables to ? # The list owner should allow users to set language options if they # want to use non-us-ascii characters in password and send it back. password = unicode(password, cset, 'replace').encode(cset, 'replace') # get the text from the template text = Utils.maketext( 'userpass.txt', { 'user': cpuser, 'listname': self.real_name, 'fqdn_lname': self.GetListEmail(), 'password': password, 'options_url': self.GetOptionsURL(user, absolute=True), 'requestaddr': requestaddr, 'owneraddr': self.GetOwnerEmail(), }, lang=lang, mlist=self) msg = Message.UserNotification(recipient, adminaddr, subject, text, lang) msg['X-No-Archive'] = 'yes' msg.send(self, verp=mm_cfg.VERP_PERSONALIZED_DELIVERIES)
def MailUserPassword(self, user): listfullname = "%s@%s" % (self.real_name, self.host_name) requestaddr = self.GetRequestEmail() # find the lowercased version of the user's address adminaddr = self.GetBouncesEmail() assert self.isMember(user) if not self.getMemberPassword(user): # The user's password somehow got corrupted. Generate a new one # for him, after logging this bogosity. syslog("error", "User %s had a false password for list %s", user, self.internal_name()) waslocked = self.Locked() if not waslocked: self.Lock() try: self.setMemberPassword(user, Utils.MakeRandomPassword()) self.Save() finally: if not waslocked: self.Unlock() # Now send the user his password cpuser = self.getMemberCPAddress(user) recipient = self.GetMemberAdminEmail(cpuser) subject = _("%(listfullname)s mailing list reminder") # Get user's language and charset lang = self.getMemberLanguage(user) cset = Utils.GetCharSet(lang) password = self.getMemberPassword(user) # TK: Make unprintables to ? # The list owner should allow users to set language options if they # want to use non-us-ascii characters in password and send it back. password = unicode(password, cset, "replace").encode(cset, "replace") # get the text from the template text = Utils.maketext( "userpass.txt", { "user": cpuser, "listname": self.real_name, "fqdn_lname": self.GetListEmail(), "password": password, "options_url": self.GetOptionsURL(user, absolute=True), "requestaddr": requestaddr, "owneraddr": self.GetOwnerEmail(), }, lang=lang, mlist=self, ) msg = Message.UserNotification(recipient, adminaddr, subject, text, lang) msg["X-No-Archive"] = "yes" msg.send(self, verp=mm_cfg.VERP_PERSONALIZED_DELIVERIES)
def ParseTag(self, template, replacements): text = Utils.maketext(template, raw=1, lang=lang, mlist=self) parts = re.split('(</?[Mm][Mm]-[^>]*>)', text) i = 1 while i < len(parts): tag = parts[i].lower() if replacements.has_key(tag): repl = replacements[tag] if isinstance(repl, type(u'')): repl = repl.encode(charset, 'replace') parts[i] = repl else: parts[i] = '' i = i + 2 return EMPTYSTRING.join(parts)
def notify_owner (self): """Send an email to the owner of the list of successful creation.""" siteowner = Utils.get_site_email(self.ml.host_name, 'owner') text = Utils.maketext( 'newlist.txt', {'listname' : self.ln, 'password' : '', 'admin_url' : self.ml.GetScriptURL('admin', absolute=1), 'listinfo_url': self.ml.GetScriptURL('listinfo', absolute=1), 'requestaddr' : self.ml.GetRequestEmail(), 'siteowner' : siteowner, }, mlist=self.ml) msg = Message.UserNotification( self.owner, siteowner, 'Your new mailing list: %s' % self.ln, text, self.ml.preferred_language) msg.send(self.ml)
def __refuse(self, request, recip, comment, origmsg=None): adminaddr = self.GetAdminEmail() text = Utils.maketext( 'refuse.txt', {'listname' : self.real_name, 'request' : request, 'reason' : comment, 'adminaddr': adminaddr, }) # add in original message, but not wrap/filled if origmsg: text = string.join([text, '---------- Original Message ----------', str(origmsg)], '\n') subject = 'Request to mailing list %s rejected' % self.real_name msg = Message.UserNotification(recip, adminaddr, subject, text) HandlerAPI.DeliverToUser(self, msg, {'_enqueue_immediate': 1})
def PrintRequests(mlist, doc): # The only types of requests we know about are add member and post. # Anything else that might have gotten in here somehow we'll just ignore # (This should never happen unless someone is hacking at the code). doc.AddItem(Header(2, 'Administrative requests for mailing list: <em>' + mlist.real_name + '</em>')) # short circuit for when there are no pending requests if not mlist.NumRequestsPending(): doc.AddItem('There are no pending requests.') doc.AddItem(mlist.GetMailmanFooter()) return doc.AddItem(Utils.maketext( 'admindbpreamble.html', {'listname': mlist.real_name}, raw=1)) doc.AddItem('.<p>') form = Form(mlist.GetScriptURL('admindb')) doc.AddItem(form) form.AddItem(SubmitButton('submit', 'Submit All Data')) # # Add the subscription request section subpendings = mlist.GetSubscriptionIds() if subpendings: form.AddItem('<hr>') form.AddItem(Center(Header(2, 'Subscription Requests'))) t = Table(border=2) t.AddRow([ Bold('Address'), Bold('Your Decision'), Bold('If you refuse this subscription, please explain (optional)') ]) for id in subpendings: PrintAddMemberRequest(mlist, id, t) form.AddItem(t) # Post holds are now handled differently heldmsgs = mlist.GetHeldMessageIds() total = len(heldmsgs) if total: count = 1 for id in heldmsgs: info = mlist.GetRecord(id) PrintPostRequest(mlist, id, info, total, count, form) count = count + 1 form.AddItem('<hr>') form.AddItem(SubmitButton('submit', 'Submit All Data')) doc.AddItem(mlist.GetMailmanFooter())
def FormatHTML(mlist, doc, template_name, template_info): doc.AddItem(Header(1,'%s:' % mlist.real_name)) doc.AddItem(Header(1, template_info)) doc.AddItem('<hr>') link = Link(mlist.GetScriptURL('admin'), _('View or edit the list configuration information.')) doc.AddItem(FontSize("+1", link)) doc.AddItem('<p>') doc.AddItem('<hr>') form = Form(mlist.GetScriptURL('edithtml') + '/' + template_name) text = Utils.maketext(template_name, raw=1, mlist=mlist) # MAS: Don't websafe twice. TextArea does it. form.AddItem(TextArea('html_code', text, rows=40, cols=75)) form.AddItem('<p>' + _('When you are done making changes...')) form.AddItem(SubmitButton('submit', _('Submit Changes'))) doc.AddItem(form)
def HoldSubscription(self, addr, fullname, password, digest, lang): # Assure that the database is open for writing self.__opendb() # Get the next unique id id = self.__nextid() # Save the information to the request database. for held subscription # entries, each record in the database will be one of the following # format: # # the time the subscription request was received # the subscriber's address # the subscriber's selected password (TBD: is this safe???) # the digest flag # the user's preferred language data = time.time(), addr, fullname, password, digest, lang self.__db[id] = (SUBSCRIPTION, data) # # TBD: this really shouldn't go here but I'm not sure where else is # appropriate. syslog('vette', '%s: held subscription request from %s', self.internal_name(), addr) # Possibly notify the administrator in default list language if self.admin_immed_notify: ## cpanel patch: is the str() handling still needed? i18n.set_language(self.preferred_language) realname = str(self.real_name) subject = _( 'New subscription request to list %(realname)s from %(addr)s') text = Utils.maketext( 'subauth.txt', { 'username': addr, 'listname': self.real_name, 'hostname': self.host_name, 'admindb_url': self.GetScriptURL('admindb', absolute=1), }, mlist=self) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. owneraddr = self.GetOwnerEmail() msg = Message.UserNotification(owneraddr, owneraddr, subject, text, self.preferred_language) msg.send(self, **{'tomoderators': 1}) # Restore the user's preferred language. i18n.set_language(lang)
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 SendSubscribeAck(self, name, password, digest, text=''): pluser = self.getMemberLanguage(name) # Need to set this here to get the proper l10n of the Subject: i18n.set_language(pluser) 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 = '' # get the text from the template text += Utils.maketext( 'subscribeack.txt', { '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 ParseTags(self, template, replacements, lang=None): if lang is None: charset = 'us-ascii' else: charset = Utils.GetCharSet(lang) text = Utils.maketext(template, raw=1, lang=lang, mlist=self) parts = re.split('(</?[Mm][Mm]-[^>]*>)', text) i = 1 while i < len(parts): tag = parts[i].lower() if replacements.has_key(tag): repl = replacements[tag] if isinstance(repl, type(u'')): repl = repl.encode(charset, 'replace') parts[i] = repl else: parts[i] = '' i = i + 2 return EMPTYSTRING.join(parts)
def HoldSubscription(self, addr, fullname, password, digest, lang): # Assure that the database is open for writing self.__opendb() # Get the next unique id id = self.__nextid() # Save the information to the request database. for held subscription # entries, each record in the database will be one of the following # format: # # the time the subscription request was received # the subscriber's address # the subscriber's selected password (TBD: is this safe???) # the digest flag # the user's preferred language data = time.time(), addr, fullname, password, digest, lang self.__db[id] = (SUBSCRIPTION, data) # # TBD: this really shouldn't go here but I'm not sure where else is # appropriate. syslog('vette', '%s: held subscription request from %s', self.internal_name(), addr) # Possibly notify the administrator in default list language if self.admin_immed_notify: ## cpanel patch: is the str() handling still needed? i18n.set_language(self.preferred_language) realname = str(self.real_name) subject = _( 'New subscription request to list %(realname)s from %(addr)s') text = Utils.maketext( 'subauth.txt', {'username' : addr, 'listname' : self.real_name, 'hostname' : self.host_name, 'admindb_url': self.GetScriptURL('admindb', absolute=1), }, mlist=self) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. owneraddr = self.GetOwnerEmail() msg = Message.UserNotification(owneraddr, owneraddr, subject, text, self.preferred_language) msg.send(self, **{'tomoderators': 1}) # Restore the user's preferred language. i18n.set_language(lang)
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 HoldSubscription(self, addr, password, digest): # assure that the database is open for writing self.__opendb() # get the next unique id id = self.__request_id() assert not self.__db.has_key(id) # # save the information to the request database. for held subscription # entries, each record in the database will be one of the following # format: # # the time the subscription request was received # the subscriber's address # the subscriber's selected password (TBD: is this safe???) # the digest flag # data = time.time(), addr, password, digest self.__db[id] = (SUBSCRIPTION, data) # # TBD: this really shouldn't go here but I'm not sure where else is # appropriate. syslog('vette', '%s: held subscription request from %s' % (self.real_name, addr)) # possibly notify the administrator if self.admin_immed_notify: subject = 'New subscription request to list %s from %s' % ( self.real_name, addr) text = Utils.maketext( 'subauth.txt', {'username' : addr, 'listname' : self.real_name, 'hostname' : self.host_name, 'admindb_url': self.GetScriptURL('admindb', absolute=1), }) adminaddr = self.GetAdminEmail() msg = Message.UserNotification(adminaddr, adminaddr, subject, text) HandlerAPI.DeliverToUser(self, msg, {'_enqueue_immediate': 1})
def process(mlist, msg, msgdata): sender = msgdata.get('original_sender', msg.GetSender()) sender = mlist.FindUser(sender) if sender and mlist.GetUserOption(sender, mm_cfg.AcknowledgePosts): subject = msg.getheader('subject') if subject: # trim off the subject prefix prefix = mlist.subject_prefix plen = len(prefix) if len(subject) > plen and subject[0:plen] == prefix: subject = subject[plen:] # get the text from the template text = Utils.maketext( 'postack.txt', {'subject' : subject, 'listname' : mlist.real_name, 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), }) # craft the outgoing message, with all headers and attributes # necessary for general delivery subject = '%s post acknowledgement' % mlist.real_name msg = Message.UserNotification(sender, mlist.GetAdminEmail(), subject, text) HandlerAPI.DeliverToUser(mlist, msg)
# Add a link back to the overview, if we're not viewing the overview! adminurl = mlist.GetScriptURL('admin', absolute=1) d = { 'listname': mlist.real_name, 'detailsurl': admindburl + '?details=instructions', 'summaryurl': admindburl, 'viewallurl': admindburl + '?details=all', 'adminurl': adminurl, 'filterurl': adminurl + '/privacy/sender', } addform = 1 if sender: esender = Utils.websafe(sender) d['description'] = _("all of %(esender)s's held messages.") doc.AddItem( Utils.maketext('admindbpreamble.html', d, raw=1, mlist=mlist)) show_sender_requests(mlist, form, sender) elif msgid: d['description'] = _('a single held message.') doc.AddItem( Utils.maketext('admindbpreamble.html', d, raw=1, mlist=mlist)) show_message_requests(mlist, form, msgid) elif details == 'all': d['description'] = _('all held messages.') doc.AddItem( Utils.maketext('admindbpreamble.html', d, raw=1, mlist=mlist)) show_detailed_requests(mlist, form) elif details == 'instructions': doc.AddItem( Utils.maketext('admindbdetails.html', d, raw=1, mlist=mlist)) addform = 0
_('Discard all messages marked <em>Defer</em>') )) # Add a link back to the overview, if we're not viewing the overview! adminurl = mlist.GetScriptURL('admin', absolute=1) d = {'listname' : mlist.real_name, 'detailsurl': admindburl + '?details=instructions', 'summaryurl': admindburl, 'viewallurl': admindburl + '?details=all', 'adminurl' : adminurl, 'filterurl' : adminurl + '/privacy/sender', } addform = 1 if sender: esender = Utils.websafe(sender) d['description'] = _("all of %(esender)s's held messages.") doc.AddItem(Utils.maketext('admindbpreamble.html', d, raw=1, mlist=mlist)) show_sender_requests(mlist, form, sender) elif msgid: d['description'] = _('a single held message.') doc.AddItem(Utils.maketext('admindbpreamble.html', d, raw=1, mlist=mlist)) show_message_requests(mlist, form, msgid) elif details == 'all': d['description'] = _('all held messages.') doc.AddItem(Utils.maketext('admindbpreamble.html', d, raw=1, mlist=mlist)) show_detailed_requests(mlist, form) elif details == 'instructions': doc.AddItem(Utils.maketext('admindbdetails.html', d, raw=1, mlist=mlist)) addform = 0
def send_i18n_digests(mlist, mboxfp): mbox = Mailbox(mboxfp) # Prepare common information (first lang/charset) lang = mlist.preferred_language lcset = Utils.GetCharSet(lang) lcset_out = Charset(lcset).output_charset or lcset # Common Information (contd) realname = mlist.real_name volume = mlist.volume issue = mlist.next_digest_number digestid = _('%(realname)s Digest, Vol %(volume)d, Issue %(issue)d') digestsubj = Header(digestid, lcset, header_name='Subject') # Set things up for the MIME digest. Only headers not added by # CookHeaders need be added here. # Date/Message-ID should be added here also. mimemsg = Message.Message() mimemsg['Content-Type'] = 'multipart/mixed' mimemsg['MIME-Version'] = '1.0' mimemsg['From'] = mlist.GetRequestEmail() mimemsg['Subject'] = digestsubj mimemsg['To'] = mlist.GetListEmail() mimemsg['Reply-To'] = mlist.GetListEmail() mimemsg['Date'] = formatdate(localtime=1) mimemsg['Message-ID'] = Utils.unique_message_id(mlist) # Set things up for the rfc1153 digest plainmsg = StringIO() rfc1153msg = Message.Message() rfc1153msg['From'] = mlist.GetRequestEmail() rfc1153msg['Subject'] = digestsubj rfc1153msg['To'] = mlist.GetListEmail() rfc1153msg['Reply-To'] = mlist.GetListEmail() rfc1153msg['Date'] = formatdate(localtime=1) rfc1153msg['Message-ID'] = Utils.unique_message_id(mlist) separator70 = '-' * 70 separator30 = '-' * 30 # In the rfc1153 digest, the masthead contains the digest boilerplate plus # any digest header. In the MIME digests, the masthead and digest header # are separate MIME subobjects. In either case, it's the first thing in # the digest, and we can calculate it now, so go ahead and add it now. mastheadtxt = Utils.maketext( 'masthead.txt', { 'real_name': mlist.real_name, 'got_list_email': mlist.GetListEmail(), 'got_listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), 'got_request_email': mlist.GetRequestEmail(), 'got_owner_email': mlist.GetOwnerEmail(), }, mlist=mlist) # MIME masthead = MIMEText(mastheadtxt, _charset=lcset) masthead['Content-Description'] = digestid mimemsg.attach(masthead) # RFC 1153 print >> plainmsg, mastheadtxt print >> plainmsg # Now add the optional digest header but only if more than whitespace. if re.sub('\s', '', mlist.digest_header): headertxt = decorate(mlist, mlist.digest_header, _('digest header')) # MIME header = MIMEText(headertxt, _charset=lcset) header['Content-Description'] = _('Digest Header') mimemsg.attach(header) # RFC 1153 print >> plainmsg, headertxt print >> plainmsg # Now we have to cruise through all the messages accumulated in the # mailbox file. We can't add these messages to the plainmsg and mimemsg # yet, because we first have to calculate the table of contents # (i.e. grok out all the Subjects). Store the messages in a list until # we're ready for them. # # Meanwhile prepare things for the table of contents toc = StringIO() print >> toc, _("Today's Topics:\n") # Now cruise through all the messages in the mailbox of digest messages, # building the MIME payload and core of the RFC 1153 digest. We'll also # accumulate Subject: headers and authors for the table-of-contents. messages = [] msgcount = 0 msg = mbox.next() while msg is not None: if msg == '': # It was an unparseable message msg = mbox.next() continue msgcount += 1 messages.append(msg) # Get the Subject header msgsubj = msg.get('subject', _('(no subject)')) subject = Utils.oneline(msgsubj, lcset) # Don't include the redundant subject prefix in the toc mo = re.match('(re:? *)?(%s)' % re.escape(mlist.subject_prefix), subject, re.IGNORECASE) if mo: subject = subject[:mo.start(2)] + subject[mo.end(2):] username = '' addresses = getaddresses([Utils.oneline(msg.get('from', ''), lcset)]) # Take only the first author we find if isinstance(addresses, ListType) and addresses: username = addresses[0][0] if not username: username = addresses[0][1] if username: username = '******' % username # Put count and Wrap the toc subject line wrapped = Utils.wrap('%2d. %s' % (msgcount, subject), 65) slines = wrapped.split('\n') # See if the user's name can fit on the last line if len(slines[-1]) + len(username) > 70: slines.append(username) else: slines[-1] += username # Add this subject to the accumulating topics first = True for line in slines: if first: print >> toc, ' ', line first = False else: print >> toc, ' ', line.lstrip() # We do not want all the headers of the original message to leak # through in the digest messages. For this phase, we'll leave the # same set of headers in both digests, i.e. those required in RFC 1153 # plus a couple of other useful ones. We also need to reorder the # headers according to RFC 1153. Later, we'll strip out headers for # for the specific MIME or plain digests. keeper = {} all_keepers = {} for header in (mm_cfg.MIME_DIGEST_KEEP_HEADERS + mm_cfg.PLAIN_DIGEST_KEEP_HEADERS): all_keepers[header] = True all_keepers = all_keepers.keys() for keep in all_keepers: keeper[keep] = msg.get_all(keep, []) # Now remove all unkempt headers :) for header in msg.keys(): del msg[header] # And add back the kept header in the RFC 1153 designated order for keep in all_keepers: for field in keeper[keep]: msg[keep] = field # And a bit of extra stuff msg['Message'] = ` msgcount ` # Get the next message in the digest mailbox msg = mbox.next() # Now we're finished with all the messages in the digest. First do some # sanity checking and then on to adding the toc. if msgcount == 0: # Why did we even get here? return toctext = to_cset_out(toc.getvalue(), lcset) # MIME tocpart = MIMEText(toctext, _charset=lcset) tocpart['Content-Description'] = _( "Today's Topics (%(msgcount)d messages)") mimemsg.attach(tocpart) # RFC 1153 print >> plainmsg, toctext print >> plainmsg # For RFC 1153 digests, we now need the standard separator print >> plainmsg, separator70 print >> plainmsg # Now go through and add each message mimedigest = MIMEBase('multipart', 'digest') mimemsg.attach(mimedigest) first = True for msg in messages: # MIME. Make a copy of the message object since the rfc1153 # processing scrubs out attachments. mimedigest.attach(MIMEMessage(copy.deepcopy(msg))) # rfc1153 if first: first = False else: print >> plainmsg, separator30 print >> plainmsg # Use Mailman.Handlers.Scrubber.process() to get plain text try: msg = scrubber(mlist, msg) except Errors.DiscardMessage: print >> plainmsg, _('[Message discarded by content filter]') continue # Honor the default setting for h in mm_cfg.PLAIN_DIGEST_KEEP_HEADERS: if msg[h]: uh = Utils.wrap('%s: %s' % (h, Utils.oneline(msg[h], lcset))) uh = '\n\t'.join(uh.split('\n')) print >> plainmsg, uh print >> plainmsg # If decoded payload is empty, this may be multipart message. # -- just stringfy it. payload = msg.get_payload(decode=True) \ or msg.as_string().split('\n\n',1)[1] mcset = msg.get_content_charset('') if mcset and mcset <> lcset and mcset <> lcset_out: try: payload = unicode(payload, mcset, 'replace').encode(lcset, 'replace') except (UnicodeError, LookupError): # TK: Message has something unknown charset. # _out means charset in 'outer world'. payload = unicode(payload, lcset_out, 'replace').encode(lcset, 'replace') print >> plainmsg, payload if not payload.endswith('\n'): print >> plainmsg # Now add the footer but only if more than whitespace. if re.sub('\s', '', mlist.digest_footer): footertxt = decorate(mlist, mlist.digest_footer, _('digest footer')) # MIME footer = MIMEText(footertxt, _charset=lcset) footer['Content-Description'] = _('Digest Footer') mimemsg.attach(footer) # RFC 1153 # MAS: There is no real place for the digest_footer in an RFC 1153 # compliant digest, so add it as an additional message with # Subject: Digest Footer print >> plainmsg, separator30 print >> plainmsg print >> plainmsg, 'Subject: ' + _('Digest Footer') print >> plainmsg print >> plainmsg, footertxt print >> plainmsg print >> plainmsg, separator30 print >> plainmsg # Do the last bit of stuff for each digest type signoff = _('End of ') + digestid # MIME # BAW: This stuff is outside the normal MIME goo, and it's what the old # MIME digester did. No one seemed to complain, probably because you # won't see it in an MUA that can't display the raw message. We've never # got complaints before, but if we do, just wax this. It's primarily # included for (marginally useful) backwards compatibility. mimemsg.postamble = signoff # rfc1153 print >> plainmsg, signoff print >> plainmsg, '*' * len(signoff) # Do our final bit of housekeeping, and then send each message to the # outgoing queue for delivery. mlist.next_digest_number += 1 virginq = get_switchboard(mm_cfg.VIRGINQUEUE_DIR) # Calculate the recipients lists plainrecips = [] mimerecips = [] drecips = mlist.getDigestMemberKeys() + mlist.one_last_digest.keys() for user in mlist.getMemberCPAddresses(drecips): # user might be None if someone who toggled off digest delivery # subsequently unsubscribed from the mailing list. Also, filter out # folks who have disabled delivery. if user is None or mlist.getDeliveryStatus(user) <> ENABLED: continue # Otherwise, decide whether they get MIME or RFC 1153 digests if mlist.getMemberOption(user, mm_cfg.DisableMime): plainrecips.append(user) else: mimerecips.append(user) # Zap this since we're now delivering the last digest to these folks. mlist.one_last_digest.clear() # MIME virginq.enqueue(mimemsg, recips=mimerecips, listname=mlist.internal_name(), isdigest=True) # RFC 1153 rfc1153msg.set_payload(to_cset_out(plainmsg.getvalue(), lcset), lcset) virginq.enqueue(rfc1153msg, recips=plainrecips, listname=mlist.internal_name(), isdigest=True)
action += '.mbox' if parts[1:]: action = os.path.join(action, SLASH.join(parts[1:])) # If we added '/index.html' to true_filename, add a slash to the URL. # We need this because we no longer add the trailing slash in the # private.html template. It's always OK to test parts[-1] since we've # already verified parts[0] is listname. The basic rule is if the # post URL (action) is a directory, it must be slash terminated, but # not if it's a file. Otherwise, relative links in the target archive # page don't work. if true_filename.endswith('/index.html') and parts[-1] <> 'index.html': action += SLASH # Escape web input parameter to avoid cross-site scripting. print Utils.maketext( 'private.html', {'action' : Utils.websafe(action), 'realname': mlist.real_name, 'message' : message, }, mlist=mlist) return lang = mlist.getMemberLanguage(username) i18n.set_language(lang) doc.set_language(lang) # Authorization confirmed... output the desired file try: ctype, enc = guess_type(path, strict=0) if ctype is None: ctype = 'text/html' if mboxfile: f = open(os.path.join(mlist.archive_dir() + '.mbox',
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 GetConfigInfo(self, mlist, category, subcat=None): if category <> 'digest': return None WIDTH = mm_cfg.TEXTFIELDWIDTH info = [ _("Batched-delivery digest characteristics."), ('digestable', mm_cfg.Toggle, (_('No'), _('Yes')), 1, _('Can list members choose to receive list traffic ' 'bunched in digests?')), ('digest_is_default', mm_cfg.Radio, (_('Regular'), _('Digest')), 0, _('Which delivery mode is the default for new users?')), ('mime_is_default_digest', mm_cfg.Radio, (_('Plain'), _('MIME')), 0, _('When receiving digests, which format is default?')), ('digest_size_threshhold', mm_cfg.Number, 3, 0, _('How big in Kb should a digest be before it gets sent out?' ' 0 implies no maximum size.')), ('digest_send_periodic', mm_cfg.Radio, (_('No'), _('Yes')), 1, _('Should a digest be dispatched daily when the size threshold ' "isn't reached?")), ('digest_header', mm_cfg.Text, (4, WIDTH), 0, _('Header added to every digest'), _("Text attached (as an initial message, before the table" " of contents) to the top of digests. ") + Utils.maketext('headfoot.html', raw=1, mlist=mlist)), ('digest_footer', mm_cfg.Text, (4, WIDTH), 0, _('Footer added to every digest'), _("Text attached (as a final message) to the bottom of digests. ") + Utils.maketext('headfoot.html', raw=1, mlist=mlist)), ('digest_volume_frequency', mm_cfg.Radio, (_('Yearly'), _('Monthly'), _('Quarterly'), _('Weekly'), _('Daily')), 0, _('How often should a new digest volume be started?'), _('''When a new digest volume is started, the volume number is incremented and the issue number is reset to 1.''')), ('_new_volume', mm_cfg.Toggle, (_('No'), _('Yes')), 0, _('Should Mailman start a new digest volume?'), _('''Setting this option instructs Mailman to start a new volume with the next digest sent out.''')), ('_send_digest_now', mm_cfg.Toggle, (_('No'), _('Yes')), 0, _('''Should Mailman send the next digest right now, if it is not empty?''')), ] ## if mm_cfg.OWNERS_CAN_ENABLE_PERSONALIZATION: ## info.extend([ ## ('digest_personalize', mm_cfg.Toggle, (_('No'), _('Yes')), 1, ## _('''Should Mailman personalize each digest delivery? ## This is often useful for announce-only lists, but <a ## href="?VARHELP=digest/digest_personalize">read the details</a> ## section for a discussion of important performance ## issues.'''), ## _("""Normally, Mailman sends the digest messages to ## the mail server in batches. This is much more efficent ## because it reduces the amount of traffic between Mailman and ## the mail server. ## <p>However, some lists can benefit from a more personalized ## approach. In this case, Mailman crafts a new message for ## each member on the digest delivery list. Turning this on ## adds a few more expansion variables that can be included in ## the <a href="?VARHELP=digest/digest_header">message header</a> ## and <a href="?VARHELP=digest/digest_footer">message footer</a> ## but it may degrade the performance of your site as ## a whole. ## <p>You need to carefully consider whether the trade-off is ## worth it, or whether there are other ways to accomplish what ## you want. You should also carefully monitor your system load ## to make sure it is acceptable. ## <p>These additional substitution variables will be available ## for your headers and footers, when this feature is enabled: ## <ul><li><b>user_address</b> - The address of the user, ## coerced to lower case. ## <li><b>user_delivered_to</b> - The case-preserved address ## that the user is subscribed with. ## <li><b>user_password</b> - The user's password. ## <li><b>user_name</b> - The user's full name. ## <li><b>user_optionsurl</b> - The url to the user's option ## page. ## """)) ## ]) return info
def sendNextNotification(self, member): info = self.getBounceInfo(member) if info is None: return reason = self.getDeliveryStatus(member) if info.noticesleft <= 0: # BAW: Remove them now, with a notification message self.ApprovedDeleteMember( member, 'disabled address', admin_notif=self.bounce_notify_owner_on_removal, userack=1) # Expunge the pending cookie for the user. We throw away the # returned data. self.pend_confirm(info.cookie) if reason == MemberAdaptor.BYBOUNCE: syslog('bounce', '%s: %s deleted after exhausting notices', self.internal_name(), member) syslog( 'subscribe', '%s: %s auto-unsubscribed [reason: %s]', self.internal_name(), member, { MemberAdaptor.BYBOUNCE: 'BYBOUNCE', MemberAdaptor.BYUSER: '******', MemberAdaptor.BYADMIN: 'BYADMIN', MemberAdaptor.UNKNOWN: 'UNKNOWN' }.get(reason, 'invalid value')) return # Send the next notification confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1), info.cookie) optionsurl = self.GetOptionsURL(member, absolute=1) reqaddr = self.GetRequestEmail() lang = self.getMemberLanguage(member) txtreason = REASONS.get(reason) if txtreason is None: txtreason = _('for unknown reasons') else: txtreason = _(txtreason) # Give a little bit more detail on bounce disables if reason == MemberAdaptor.BYBOUNCE: date = time.strftime('%d-%b-%Y', time.localtime(Utils.midnight(info.date))) extra = _(' The last bounce received from you was dated %(date)s') txtreason += extra text = Utils.maketext('disabled.txt', { 'listname': self.real_name, 'noticesleft': info.noticesleft, 'confirmurl': confirmurl, 'optionsurl': optionsurl, 'password': self.getMemberPassword(member), 'owneraddr': self.GetOwnerEmail(), 'reason': txtreason, }, lang=lang, mlist=self) msg = Message.UserNotification(member, reqaddr, text=text, lang=lang) # BAW: See the comment in MailList.py ChangeMemberAddress() for why we # set the Subject this way. del msg['subject'] msg['Subject'] = 'confirm ' + info.cookie # Send without Precedence: bulk. Bug #808821. msg.send(self, noprecedence=True) info.noticesleft -= 1 info.lastnotice = time.localtime()[:3] # In case the MemberAdaptor stores bounce info externally to # the list, we need to tell it to update self.setBounceInfo(member, info)
def sendNextNotification(self, member): info = self.getBounceInfo(member) if info is None: return reason = self.getDeliveryStatus(member) if info.noticesleft <= 0: # BAW: Remove them now, with a notification message self.ApprovedDeleteMember( member, "disabled address", admin_notif=self.bounce_notify_owner_on_removal, userack=1 ) # Expunge the pending cookie for the user. We throw away the # returned data. self.pend_confirm(info.cookie) if reason == MemberAdaptor.BYBOUNCE: syslog("bounce", "%s: %s deleted after exhausting notices", self.internal_name(), member) syslog( "subscribe", "%s: %s auto-unsubscribed [reason: %s]", self.internal_name(), member, { MemberAdaptor.BYBOUNCE: "BYBOUNCE", MemberAdaptor.BYUSER: "******", MemberAdaptor.BYADMIN: "BYADMIN", MemberAdaptor.UNKNOWN: "UNKNOWN", }.get(reason, "invalid value"), ) return # Send the next notification confirmurl = "%s/%s" % (self.GetScriptURL("confirm", absolute=1), info.cookie) optionsurl = self.GetOptionsURL(member, absolute=1) reqaddr = self.GetRequestEmail() lang = self.getMemberLanguage(member) txtreason = REASONS.get(reason) if txtreason is None: txtreason = _("for unknown reasons") else: txtreason = _(txtreason) # Give a little bit more detail on bounce disables if reason == MemberAdaptor.BYBOUNCE: date = time.strftime("%d-%b-%Y", time.localtime(Utils.midnight(info.date))) extra = _(" The last bounce received from you was dated %(date)s") txtreason += extra text = Utils.maketext( "disabled.txt", { "listname": self.real_name, "noticesleft": info.noticesleft, "confirmurl": confirmurl, "optionsurl": optionsurl, "password": self.getMemberPassword(member), "owneraddr": self.GetOwnerEmail(), "reason": txtreason, }, lang=lang, mlist=self, ) msg = Message.UserNotification(member, reqaddr, text=text, lang=lang) # BAW: See the comment in MailList.py ChangeMemberAddress() for why we # set the Subject this way. del msg["subject"] msg["Subject"] = "confirm " + info.cookie # Send without Precedence: bulk. Bug #808821. msg.send(self, noprecedence=True) info.noticesleft -= 1 info.lastnotice = time.localtime()[:3] # In case the MemberAdaptor stores bounce info externally to # the list, we need to tell it to update self.setBounceInfo(member, info)
def main(): doc = Document() doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) parts = Utils.GetPathPieces() if not parts: doc.SetTitle(_("Private Archive Error")) doc.AddItem(Header(3, _("You must specify a list."))) print(doc.Format()) return path = os.environ.get('PATH_INFO') tpath = true_path(path) if tpath != path[1:]: msg = _('Private archive - "./" and "../" not allowed in URL.') doc.SetTitle(msg) doc.AddItem(Header(2, msg)) print(doc.Format()) syslog('mischief', 'Private archive hostile path: %s', path) return # BAW: This needs to be converted to the Site module abstraction true_filename = os.path.join( mm_cfg.PRIVATE_ARCHIVE_FILE_DIR, tpath) listname = parts[0].lower() mboxfile = '' if len(parts) > 1: mboxfile = parts[1] # See if it's the list's mbox file is being requested if listname.endswith('.mbox') and mboxfile.endswith('.mbox') and \ listname[:-5] == mboxfile[:-5]: listname = listname[:-5] else: mboxfile = '' # If it's a directory, we have to append index.html in this script. We # must also check for a gzipped file, because the text archives are # usually stored in compressed form. if os.path.isdir(true_filename): true_filename = true_filename + '/index.html' if not os.path.exists(true_filename) and \ os.path.exists(true_filename + '.gz'): true_filename = true_filename + '.gz' try: mlist = MailList.MailList(listname, lock=0) except Errors.MMListError as e: # Avoid cross-site scripting attacks safelistname = Utils.websafe(listname) msg = _('No such list <em>%(safelistname)s</em>') doc.SetTitle(_("Private Archive Error - %(msg)s")) doc.AddItem(Header(2, msg)) # Send this with a 404 status. print('Status: 404 Not Found') print(doc.Format()) syslog('error', 'private: No such list "%s": %s\n', listname, e) return i18n.set_language(mlist.preferred_language) doc.set_language(mlist.preferred_language) cgidata = cgi.FieldStorage() try: username = cgidata.getfirst('username', '') except TypeError: # Someone crafted a POST with a bad Content-Type:. doc.AddItem(Header(2, _("Error"))) doc.AddItem(Bold(_('Invalid options to CGI script.'))) # Send this with a 400 status. print('Status: 400 Bad Request') print(doc.Format()) return password = cgidata.getfirst('password', '') is_auth = 0 realname = mlist.real_name message = '' if not mlist.WebAuthenticate((mm_cfg.AuthUser, mm_cfg.AuthListModerator, mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin), password, username): if 'submit' in cgidata: # This is a re-authorization attempt message = Bold(FontSize('+1', _('Authorization failed.'))).Format() remote = os.environ.get('HTTP_FORWARDED_FOR', os.environ.get('HTTP_X_FORWARDED_FOR', os.environ.get('REMOTE_ADDR', 'unidentified origin'))) syslog('security', 'Authorization failed (private): user=%s: list=%s: remote=%s', username, listname, remote) # give an HTTP 401 for authentication failure print('Status: 401 Unauthorized') # Are we processing a password reminder from the login screen? if 'login-remind' in cgidata: if username: message = Bold(FontSize('+1', _("""If you are a list member, your password has been emailed to you."""))).Format() else: message = Bold(FontSize('+1', _('Please enter your email address'))).Format() if mlist.isMember(username): mlist.MailUserPassword(username) elif username: # Not a member if mlist.private_roster == 0: # Public rosters safeuser = Utils.websafe(username) message = Bold(FontSize('+1', _('No such member: %(safeuser)s.'))).Format() else: syslog('mischief', 'Reminder attempt of non-member w/ private rosters: %s', username) # Output the password form charset = Utils.GetCharSet(mlist.preferred_language) print('Content-type: text/html; charset=' + charset + '\n\n') # Put the original full path in the authorization form, but avoid # trailing slash if we're not adding parts. We add it below. action = mlist.GetScriptURL('private', absolute=1) if mboxfile: action += '.mbox' if parts[1:]: action = os.path.join(action, SLASH.join(parts[1:])) # If we added '/index.html' to true_filename, add a slash to the URL. # We need this because we no longer add the trailing slash in the # private.html template. It's always OK to test parts[-1] since we've # already verified parts[0] is listname. The basic rule is if the # post URL (action) is a directory, it must be slash terminated, but # not if it's a file. Otherwise, relative links in the target archive # page don't work. if true_filename.endswith('/index.html') and parts[-1] != 'index.html': action += SLASH # Escape web input parameter to avoid cross-site scripting. print(Utils.maketext( 'private.html', {'action' : Utils.websafe(action), 'realname': mlist.real_name, 'message' : message, }, mlist=mlist)) return lang = mlist.getMemberLanguage(username) i18n.set_language(lang) doc.set_language(lang) # Authorization confirmed... output the desired file try: ctype, enc = guess_type(path, strict=0) if ctype is None: ctype = 'text/html' if mboxfile: f = open(os.path.join(mlist.archive_dir() + '.mbox', mlist.internal_name() + '.mbox')) ctype = 'text/plain' elif true_filename.endswith('.gz'): import gzip f = gzip.open(true_filename, 'r') else: f = open(true_filename, 'r') except IOError: msg = _('Private archive file not found') doc.SetTitle(msg) doc.AddItem(Header(2, msg)) print('Status: 404 Not Found') print(doc.Format()) syslog('error', 'Private archive file not found: %s', true_filename) else: print('Content-type: %s\n' % ctype) sys.stdout.write(f.read()) f.close()
def GetConfigInfo(self, mlist, category, subcat=None): if category <> 'nondigest': return None WIDTH = mm_cfg.TEXTFIELDWIDTH info = [ _("Policies concerning immediately delivered list traffic."), ('nondigestable', mm_cfg.Toggle, (_('No'), _('Yes')), 1, _("""Can subscribers choose to receive mail immediately, rather than in batched digests?""")), ] if mm_cfg.OWNERS_CAN_ENABLE_PERSONALIZATION: info.extend([ ('personalize', mm_cfg.Radio, (_('No'), _('Yes'), _('Full Personalization')), 1, _('''Should Mailman personalize each non-digest delivery? This is often useful for announce-only lists, but <a href="?VARHELP=nondigest/personalize">read the details</a> section for a discussion of important performance issues.'''), _("""Normally, Mailman sends the regular delivery messages to the mail server in batches. This is much more efficent because it reduces the amount of traffic between Mailman and the mail server. <p>However, some lists can benefit from a more personalized approach. In this case, Mailman crafts a new message for each member on the regular delivery list. Turning this feature on may degrade the performance of your site, so you need to carefully consider whether the trade-off is worth it, or whether there are other ways to accomplish what you want. You should also carefully monitor your system load to make sure it is acceptable. <p>Select <em>No</em> to disable personalization and send messages to the members in batches. Select <em>Yes</em> to personalize deliveries and allow additional substitution variables in message headers and footers (see below). In addition, by selecting <em>Full Personalization</em>, the <code>To</code> header of posted messages will be modified to include the member's address instead of the list's posting address. <p>When personalization is enabled, a few more expansion variables can be included in the <a href="?VARHELP=nondigest/msg_header">message header</a> and <a href="?VARHELP=nondigest/msg_footer">message footer</a>. <p>These additional substitution variables will be available for your headers and footers, when this feature is enabled: <ul><li><b>user_address</b> - The address of the user, coerced to lower case. <li><b>user_delivered_to</b> - The case-preserved address that the user is subscribed with. <li><b>user_password</b> - The user's password. <li><b>user_name</b> - The user's full name. <li><b>user_optionsurl</b> - The url to the user's option page. </ul> """)) ]) # BAW: for very dumb reasons, we want the `personalize' attribute to # show up before the msg_header and msg_footer attrs, otherwise we'll # get a bogus warning if the header/footer contains a personalization # substitution variable, and we're transitioning from no # personalization to personalization enabled. headfoot = Utils.maketext('headfoot.html', mlist=mlist, raw=1) if mm_cfg.OWNERS_CAN_ENABLE_PERSONALIZATION: extra = _("""\ When <a href="?VARHELP=nondigest/personalize">personalization</a> is enabled for this list, additional substitution variables are allowed in your headers and footers: <ul><li><b>user_address</b> - The address of the user, coerced to lower case. <li><b>user_delivered_to</b> - The case-preserved address that the user is subscribed with. <li><b>user_password</b> - The user's password. <li><b>user_name</b> - The user's full name. <li><b>user_optionsurl</b> - The url to the user's option page. </ul> """) else: extra = '' info.extend([('msg_header', mm_cfg.Text, (10, WIDTH), 0, _('Header added to mail sent to regular list members'), _('''Text prepended to the top of every immediately-delivery message. ''') + headfoot + extra), ('msg_footer', mm_cfg.Text, (10, WIDTH), 0, _('Footer added to mail sent to regular list members'), _('''Text appended to the bottom of every immediately-delivery message. ''') + headfoot + extra), ]) info.extend([ ('scrub_nondigest', mm_cfg.Toggle, (_('No'), _('Yes')), 0, _('Scrub attachments of regular delivery message?'), _('''When you scrub attachments, they are stored in the archive area and links are made in the message so that the member can access them via a web browser. If you want the attachments to totally disappear, you can use content filtering options.''')), ]) info.extend([ _('Sibling lists'), ('regular_exclude_lists', mm_cfg.EmailList, (3, WIDTH), 0, _("""Other mailing lists on this site whose members are excluded from the regular (non-digest) delivery if those list addresses appear in a To: or Cc: header."""), _("""The list addresses should be written in full mail address format (e.g. [email protected]). Do not specify this list address mutually in the exclude list configuration page of the other list, or members of both lists won't get any message. Note also that the site administrator may prohibit cross domain siblings.""")), ('regular_exclude_ignore', mm_cfg.Toggle, (_('No'), _('Yes')), 0, _("""Ignore regular_exclude_lists of which the poster is not a member."""), _("""If a post is addressed to this list and to one or more of the exclude lists, regular members of those lists will not be sent the post from this list, but if the poster is not a member of an excluded list, the post may not be accepted by that list which leaves the members of that list with no copy of the post. Setting this to Yes ignores any of the exclude lists of which the poster is not a member.""")), ('regular_include_lists', mm_cfg.EmailList, (3, WIDTH), 0, _("""Other mailing lists on this site whose members are included in the regular (non-digest) delivery if those list addresses don't appear in a To: or Cc: header."""), _("""The list addresses should be written in full mail address format (e.g. [email protected]). Note also that the site administrator may prohibit cross domain siblings.""")), ]) return info
def GetConfigInfo(self, mlist, category, subcat=None): if category <> "digest": return None WIDTH = mm_cfg.TEXTFIELDWIDTH info = [ _("Batched-delivery digest characteristics."), ( "digestable", mm_cfg.Toggle, (_("No"), _("Yes")), 1, _("Can list members choose to receive list traffic " "bunched in digests?"), ), ( "digest_is_default", mm_cfg.Radio, (_("Regular"), _("Digest")), 0, _("Which delivery mode is the default for new users?"), ), ( "mime_is_default_digest", mm_cfg.Radio, (_("Plain"), _("MIME")), 0, _("When receiving digests, which format is default?"), ), ( "digest_size_threshhold", mm_cfg.Number, 3, 0, _("How big in Kb should a digest be before it gets sent out?"), ), # Should offer a 'set to 0' for no size threshhold. ( "digest_send_periodic", mm_cfg.Radio, (_("No"), _("Yes")), 1, _("Should a digest be dispatched daily when the size threshold " "isn't reached?"), ), ( "digest_header", mm_cfg.Text, (4, WIDTH), 0, _("Header added to every digest"), _("Text attached (as an initial message, before the table" " of contents) to the top of digests. ") + Utils.maketext("headfoot.html", raw=1, mlist=mlist), ), ( "digest_footer", mm_cfg.Text, (4, WIDTH), 0, _("Footer added to every digest"), _("Text attached (as a final message) to the bottom of digests. ") + Utils.maketext("headfoot.html", raw=1, mlist=mlist), ), ( "digest_volume_frequency", mm_cfg.Radio, (_("Yearly"), _("Monthly"), _("Quarterly"), _("Weekly"), _("Daily")), 0, _("How often should a new digest volume be started?"), _( """When a new digest volume is started, the volume number is incremented and the issue number is reset to 1.""" ), ), ( "_new_volume", mm_cfg.Toggle, (_("No"), _("Yes")), 0, _("Should Mailman start a new digest volume?"), _( """Setting this option instructs Mailman to start a new volume with the next digest sent out.""" ), ), ( "_send_digest_now", mm_cfg.Toggle, (_("No"), _("Yes")), 0, _( """Should Mailman send the next digest right now, if it is not empty?""" ), ), ] ## if mm_cfg.OWNERS_CAN_ENABLE_PERSONALIZATION: ## info.extend([ ## ('digest_personalize', mm_cfg.Toggle, (_('No'), _('Yes')), 1, ## _('''Should Mailman personalize each digest delivery? ## This is often useful for announce-only lists, but <a ## href="?VARHELP=digest/digest_personalize">read the details</a> ## section for a discussion of important performance ## issues.'''), ## _("""Normally, Mailman sends the digest messages to ## the mail server in batches. This is much more efficent ## because it reduces the amount of traffic between Mailman and ## the mail server. ## <p>However, some lists can benefit from a more personalized ## approach. In this case, Mailman crafts a new message for ## each member on the digest delivery list. Turning this on ## adds a few more expansion variables that can be included in ## the <a href="?VARHELP=digest/digest_header">message header</a> ## and <a href="?VARHELP=digest/digest_footer">message footer</a> ## but it may degrade the performance of your site as ## a whole. ## <p>You need to carefully consider whether the trade-off is ## worth it, or whether there are other ways to accomplish what ## you want. You should also carefully monitor your system load ## to make sure it is acceptable. ## <p>These additional substitution variables will be available ## for your headers and footers, when this feature is enabled: ## <ul><li><b>user_address</b> - The address of the user, ## coerced to lower case. ## <li><b>user_delivered_to</b> - The case-preserved address ## that the user is subscribed with. ## <li><b>user_password</b> - The user's password. ## <li><b>user_name</b> - The user's full name. ## <li><b>user_optionsurl</b> - The url to the user's option ## page. ## """)) ## ]) return info
def send_i18n_digests(mlist, mboxfp): mbox = Mailbox(mboxfp) # Prepare common information (first lang/charset) lang = mlist.preferred_language lcset = Utils.GetCharSet(lang) lcset_out = Charset(lcset).output_charset or lcset # Common Information (contd) realname = mlist.real_name volume = mlist.volume issue = mlist.next_digest_number digestid = _('%(realname)s Digest, Vol %(volume)d, Issue %(issue)d') digestsubj = Header(digestid, lcset, header_name='Subject') # Set things up for the MIME digest. Only headers not added by # CookHeaders need be added here. # Date/Message-ID should be added here also. mimemsg = Message.Message() mimemsg['Content-Type'] = 'multipart/mixed' mimemsg['MIME-Version'] = '1.0' mimemsg['From'] = mlist.GetRequestEmail() mimemsg['Subject'] = digestsubj mimemsg['To'] = mlist.GetListEmail() mimemsg['Reply-To'] = mlist.GetListEmail() mimemsg['Date'] = formatdate(localtime=1) mimemsg['Message-ID'] = Utils.unique_message_id(mlist) # Set things up for the rfc1153 digest plainmsg = StringIO() rfc1153msg = Message.Message() rfc1153msg['From'] = mlist.GetRequestEmail() rfc1153msg['Subject'] = digestsubj rfc1153msg['To'] = mlist.GetListEmail() rfc1153msg['Reply-To'] = mlist.GetListEmail() rfc1153msg['Date'] = formatdate(localtime=1) rfc1153msg['Message-ID'] = Utils.unique_message_id(mlist) separator70 = '-' * 70 separator30 = '-' * 30 # In the rfc1153 digest, the masthead contains the digest boilerplate plus # any digest header. In the MIME digests, the masthead and digest header # are separate MIME subobjects. In either case, it's the first thing in # the digest, and we can calculate it now, so go ahead and add it now. mastheadtxt = Utils.maketext( 'masthead.txt', {'real_name' : mlist.real_name, 'got_list_email': mlist.GetListEmail(), 'got_listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), 'got_request_email': mlist.GetRequestEmail(), 'got_owner_email': mlist.GetOwnerEmail(), }, mlist=mlist) # MIME masthead = MIMEText(mastheadtxt, _charset=lcset) masthead['Content-Description'] = digestid mimemsg.attach(masthead) # RFC 1153 print >> plainmsg, mastheadtxt print >> plainmsg # Now add the optional digest header but only if more than whitespace. if re.sub('\s', '', mlist.digest_header): headertxt = decorate(mlist, mlist.digest_header, _('digest header')) # MIME header = MIMEText(headertxt, _charset=lcset) header['Content-Description'] = _('Digest Header') mimemsg.attach(header) # RFC 1153 print >> plainmsg, headertxt print >> plainmsg # Now we have to cruise through all the messages accumulated in the # mailbox file. We can't add these messages to the plainmsg and mimemsg # yet, because we first have to calculate the table of contents # (i.e. grok out all the Subjects). Store the messages in a list until # we're ready for them. # # Meanwhile prepare things for the table of contents toc = StringIO() print >> toc, _("Today's Topics:\n") # Now cruise through all the messages in the mailbox of digest messages, # building the MIME payload and core of the RFC 1153 digest. We'll also # accumulate Subject: headers and authors for the table-of-contents. messages = [] msgcount = 0 msg = mbox.next() while msg is not None: if msg == '': # It was an unparseable message msg = mbox.next() continue msgcount += 1 messages.append(msg) # Get the Subject header msgsubj = msg.get('subject', _('(no subject)')) subject = Utils.oneline(msgsubj, lcset) # Don't include the redundant subject prefix in the toc mo = re.match('(re:? *)?(%s)' % re.escape(mlist.subject_prefix), subject, re.IGNORECASE) if mo: subject = subject[:mo.start(2)] + subject[mo.end(2):] username = '' addresses = getaddresses([Utils.oneline(msg.get('from', ''), lcset)]) # Take only the first author we find if isinstance(addresses, ListType) and addresses: username = addresses[0][0] if not username: username = addresses[0][1] if username: username = '******' % username # Put count and Wrap the toc subject line wrapped = Utils.wrap('%2d. %s' % (msgcount, subject), 65) slines = wrapped.split('\n') # See if the user's name can fit on the last line if len(slines[-1]) + len(username) > 70: slines.append(username) else: slines[-1] += username # Add this subject to the accumulating topics first = True for line in slines: if first: print >> toc, ' ', line first = False else: print >> toc, ' ', line.lstrip() # We do not want all the headers of the original message to leak # through in the digest messages. For this phase, we'll leave the # same set of headers in both digests, i.e. those required in RFC 1153 # plus a couple of other useful ones. We also need to reorder the # headers according to RFC 1153. Later, we'll strip out headers for # for the specific MIME or plain digests. keeper = {} all_keepers = {} for header in (mm_cfg.MIME_DIGEST_KEEP_HEADERS + mm_cfg.PLAIN_DIGEST_KEEP_HEADERS): all_keepers[header] = True all_keepers = all_keepers.keys() for keep in all_keepers: keeper[keep] = msg.get_all(keep, []) # Now remove all unkempt headers :) for header in msg.keys(): del msg[header] # And add back the kept header in the RFC 1153 designated order for keep in all_keepers: for field in keeper[keep]: msg[keep] = field # And a bit of extra stuff msg['Message'] = `msgcount` # Get the next message in the digest mailbox msg = mbox.next() # Now we're finished with all the messages in the digest. First do some # sanity checking and then on to adding the toc. if msgcount == 0: # Why did we even get here? return toctext = to_cset_out(toc.getvalue(), lcset) # MIME tocpart = MIMEText(toctext, _charset=lcset) tocpart['Content-Description']= _("Today's Topics (%(msgcount)d messages)") mimemsg.attach(tocpart) # RFC 1153 print >> plainmsg, toctext print >> plainmsg # For RFC 1153 digests, we now need the standard separator print >> plainmsg, separator70 print >> plainmsg # Now go through and add each message mimedigest = MIMEBase('multipart', 'digest') mimemsg.attach(mimedigest) first = True for msg in messages: # MIME. Make a copy of the message object since the rfc1153 # processing scrubs out attachments. mimedigest.attach(MIMEMessage(copy.deepcopy(msg))) # rfc1153 if first: first = False else: print >> plainmsg, separator30 print >> plainmsg # Use Mailman.Handlers.Scrubber.process() to get plain text try: msg = scrubber(mlist, msg) except Errors.DiscardMessage: print >> plainmsg, _('[Message discarded by content filter]') continue # Honor the default setting for h in mm_cfg.PLAIN_DIGEST_KEEP_HEADERS: if msg[h]: uh = Utils.wrap('%s: %s' % (h, Utils.oneline(msg[h], lcset))) uh = '\n\t'.join(uh.split('\n')) print >> plainmsg, uh print >> plainmsg # If decoded payload is empty, this may be multipart message. # -- just stringfy it. payload = msg.get_payload(decode=True) \ or msg.as_string().split('\n\n',1)[1] mcset = msg.get_content_charset('') if mcset and mcset <> lcset and mcset <> lcset_out: try: payload = unicode(payload, mcset, 'replace' ).encode(lcset, 'replace') except (UnicodeError, LookupError): # TK: Message has something unknown charset. # _out means charset in 'outer world'. payload = unicode(payload, lcset_out, 'replace' ).encode(lcset, 'replace') print >> plainmsg, payload if not payload.endswith('\n'): print >> plainmsg # Now add the footer but only if more than whitespace. if re.sub('\s', '', mlist.digest_footer): footertxt = decorate(mlist, mlist.digest_footer, _('digest footer')) # MIME footer = MIMEText(footertxt, _charset=lcset) footer['Content-Description'] = _('Digest Footer') mimemsg.attach(footer) # RFC 1153 # MAS: There is no real place for the digest_footer in an RFC 1153 # compliant digest, so add it as an additional message with # Subject: Digest Footer print >> plainmsg, separator30 print >> plainmsg print >> plainmsg, 'Subject: ' + _('Digest Footer') print >> plainmsg print >> plainmsg, footertxt print >> plainmsg print >> plainmsg, separator30 print >> plainmsg # Do the last bit of stuff for each digest type signoff = _('End of ') + digestid # MIME # BAW: This stuff is outside the normal MIME goo, and it's what the old # MIME digester did. No one seemed to complain, probably because you # won't see it in an MUA that can't display the raw message. We've never # got complaints before, but if we do, just wax this. It's primarily # included for (marginally useful) backwards compatibility. mimemsg.postamble = signoff # rfc1153 print >> plainmsg, signoff print >> plainmsg, '*' * len(signoff) # Do our final bit of housekeeping, and then send each message to the # outgoing queue for delivery. mlist.next_digest_number += 1 virginq = get_switchboard(mm_cfg.VIRGINQUEUE_DIR) # Calculate the recipients lists plainrecips = [] mimerecips = [] drecips = mlist.getDigestMemberKeys() + mlist.one_last_digest.keys() for user in mlist.getMemberCPAddresses(drecips): # user might be None if someone who toggled off digest delivery # subsequently unsubscribed from the mailing list. Also, filter out # folks who have disabled delivery. if user is None or mlist.getDeliveryStatus(user) <> ENABLED: continue # Otherwise, decide whether they get MIME or RFC 1153 digests if mlist.getMemberOption(user, mm_cfg.DisableMime): plainrecips.append(user) else: mimerecips.append(user) # Zap this since we're now delivering the last digest to these folks. mlist.one_last_digest.clear() # MIME virginq.enqueue(mimemsg, recips=mimerecips, listname=mlist.internal_name(), isdigest=True) # RFC 1153 rfc1153msg.set_payload(to_cset_out(plainmsg.getvalue(), lcset), lcset) virginq.enqueue(rfc1153msg, recips=plainrecips, listname=mlist.internal_name(), isdigest=True)
def GetConfigInfo(self, mlist, category, subcat=None): if category <> 'nondigest': return None WIDTH = mm_cfg.TEXTFIELDWIDTH info = [ _("Policies concerning immediately delivered list traffic."), ('nondigestable', mm_cfg.Toggle, (_('No'), _('Yes')), 1, _("""Can subscribers choose to receive mail immediately, rather than in batched digests?""")), ] if mm_cfg.OWNERS_CAN_ENABLE_PERSONALIZATION: info.extend([ ('personalize', mm_cfg.Radio, (_('No'), _('Yes'), _('Full Personalization')), 1, _('''Should Mailman personalize each non-digest delivery? This is often useful for announce-only lists, but <a href="?VARHELP=nondigest/personalize">read the details</a> section for a discussion of important performance issues.'''), _("""Normally, Mailman sends the regular delivery messages to the mail server in batches. This is much more efficent because it reduces the amount of traffic between Mailman and the mail server. <p>However, some lists can benefit from a more personalized approach. In this case, Mailman crafts a new message for each member on the regular delivery list. Turning this feature on may degrade the performance of your site, so you need to carefully consider whether the trade-off is worth it, or whether there are other ways to accomplish what you want. You should also carefully monitor your system load to make sure it is acceptable. <p>Select <em>No</em> to disable personalization and send messages to the members in batches. Select <em>Yes</em> to personalize deliveries and allow additional substitution variables in message headers and footers (see below). In addition, by selecting <em>Full Personalization</em>, the <code>To</code> header of posted messages will be modified to include the member's address instead of the list's posting address. <p>When personalization is enabled, a few more expansion variables can be included in the <a href="?VARHELP=nondigest/msg_header">message header</a> and <a href="?VARHELP=nondigest/msg_footer">message footer</a>. <p>These additional substitution variables will be available for your headers and footers, when this feature is enabled: <ul><li><b>user_address</b> - The address of the user, coerced to lower case. <li><b>user_delivered_to</b> - The case-preserved address that the user is subscribed with. <li><b>user_password</b> - The user's password. <li><b>user_name</b> - The user's full name. <li><b>user_optionsurl</b> - The url to the user's option page. </ul> """)) ]) # BAW: for very dumb reasons, we want the `personalize' attribute to # show up before the msg_header and msg_footer attrs, otherwise we'll # get a bogus warning if the header/footer contains a personalization # substitution variable, and we're transitioning from no # personalization to personalization enabled. headfoot = Utils.maketext('headfoot.html', mlist=mlist, raw=1) if mm_cfg.OWNERS_CAN_ENABLE_PERSONALIZATION: extra = _("""\ When <a href="?VARHELP=nondigest/personalize">personalization</a> is enabled for this list, additional substitution variables are allowed in your headers and footers: <ul><li><b>user_address</b> - The address of the user, coerced to lower case. <li><b>user_delivered_to</b> - The case-preserved address that the user is subscribed with. <li><b>user_password</b> - The user's password. <li><b>user_name</b> - The user's full name. <li><b>user_optionsurl</b> - The url to the user's option page. </ul> """) else: extra = '' info.extend([ ('msg_header', mm_cfg.Text, (10, WIDTH), 0, _('Header added to mail sent to regular list members'), _('''Text prepended to the top of every immediately-delivery message. ''') + headfoot + extra), ('msg_footer', mm_cfg.Text, (10, WIDTH), 0, _('Footer added to mail sent to regular list members'), _('''Text appended to the bottom of every immediately-delivery message. ''') + headfoot + extra), ]) info.extend([ ('scrub_nondigest', mm_cfg.Toggle, (_('No'), _('Yes')), 0, _('Scrub attachments of regular delivery message?'), _('''When you scrub attachments, they are stored in the archive area and links are made in the message so that the member can access them via a web browser. If you want the attachments to totally disappear, you can use content filtering options.''')), ]) info.extend([ _('Sibling lists'), ('regular_exclude_lists', mm_cfg.EmailList, (3, WIDTH), 0, _("""Other mailing lists on this site whose members are excluded from the regular (non-digest) delivery if those list addresses appear in a To: or Cc: header."""), _("""The list addresses should be written in full mail address format (e.g. [email protected]). Do not specify this list address mutually in the exclude list configuration page of the other list, or members of both lists won't get any message. Note also that the site administrator may prohibit cross domain siblings.""")), ('regular_exclude_ignore', mm_cfg.Toggle, (_('No'), _('Yes')), 0, _("""Ignore regular_exclude_lists of which the poster is not a member."""), _("""If a post is addressed to this list and to one or more of the exclude lists, regular members of those lists will not be sent the post from this list, but if the poster is not a member of an excluded list, the post may not be accepted by that list which leaves the members of that list with no copy of the post. Setting this to Yes ignores any of the exclude lists of which the poster is not a member.""")), ('regular_include_lists', mm_cfg.EmailList, (3, WIDTH), 0, _("""Other mailing lists on this site whose members are included in the regular (non-digest) delivery if those list addresses don't appear in a To: or Cc: header."""), _("""The list addresses should be written in full mail address format (e.g. [email protected]). Note also that the site administrator may prohibit cross domain siblings.""")), ]) return info
class Archiver: # # Interface to Pipermail. HyperArch.py uses this method to get the # archive directory for the mailing list # def InitVars(self): # Configurable self.archive = mm_cfg.DEFAULT_ARCHIVE # 0=public, 1=private: self.archive_private = mm_cfg.DEFAULT_ARCHIVE_PRIVATE self.archive_volume_frequency = \ mm_cfg.DEFAULT_ARCHIVE_VOLUME_FREQUENCY # The archive file structure by default is: # # archives/ # private/ # listname.mbox/ # listname.mbox # listname/ # lots-of-pipermail-stuff # public/ # listname.mbox@ -> ../private/listname.mbox # listname@ -> ../private/listname # # IOW, the mbox and pipermail archives are always stored in the # private archive for the list. This is safe because archives/private # is always set to o-rx. Public archives have a symlink to get around # the private directory, pointing directly to the private/listname # which has o+rx permissions. Private archives do not have the # symbolic links. omask = os.umask(0) try: try: os.mkdir(self.archive_dir() + '.mbox', 02775) except OSError, e: if e.errno <> errno.EEXIST: raise # We also create an empty pipermail archive directory into # which we'll drop an empty index.html file into. This is so # that lists that have not yet received a posting have # /something/ as their index.html, and don't just get a 404. try: os.mkdir(self.archive_dir(), 02775) except OSError, e: if e.errno <> errno.EEXIST: raise # See if there's an index.html file there already and if not, # write in the empty archive notice. indexfile = os.path.join(self.archive_dir(), 'index.html') fp = None try: fp = open(indexfile) except IOError, e: if e.errno <> errno.ENOENT: raise omask = os.umask(002) try: fp = open(indexfile, 'w') finally: os.umask(omask) fp.write( Utils.maketext( 'emptyarchive.html', { 'listname': self.real_name, 'listinfo': self.GetScriptURL('listinfo', absolute=1), }, mlist=self))
def sendNextNotification(self, member): info = self.getBounceInfo(member) if info is None: return reason = self.getDeliveryStatus(member) if info.noticesleft <= 0: # BAW: Remove them now, with a notification message self.ApprovedDeleteMember( member, 'disabled address', admin_notif=self.bounce_notify_owner_on_removal, userack=1) # Expunge the pending cookie for the user. We throw away the # returned data. self.pend_confirm(info.cookie) if reason == MemberAdaptor.BYBOUNCE: syslog('bounce', '%s: %s deleted after exhausting notices', self.internal_name(), member) syslog('subscribe', '%s: %s auto-unsubscribed [reason: %s]', self.internal_name(), member, {MemberAdaptor.BYBOUNCE: 'BYBOUNCE', MemberAdaptor.BYUSER: '******', MemberAdaptor.BYADMIN: 'BYADMIN', MemberAdaptor.UNKNOWN: 'UNKNOWN'}.get( reason, 'invalid value')) return # Send the next notification confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1), info.cookie) optionsurl = self.GetOptionsURL(member, absolute=1) reqaddr = self.GetRequestEmail() lang = self.getMemberLanguage(member) txtreason = REASONS.get(reason) if txtreason is None: txtreason = _('for unknown reasons') else: txtreason = _(txtreason) # Give a little bit more detail on bounce disables if reason == MemberAdaptor.BYBOUNCE: date = time.strftime('%d-%b-%Y', time.localtime(Utils.midnight(info.date))) extra = _(' The last bounce received from you was dated %(date)s') txtreason += extra text = Utils.maketext( 'disabled.txt', {'listname' : self.real_name, 'noticesleft': info.noticesleft, 'confirmurl' : confirmurl, 'optionsurl' : optionsurl, 'password' : self.getMemberPassword(member), 'owneraddr' : self.GetOwnerEmail(), 'reason' : txtreason, }, lang=lang, mlist=self) msg = Message.UserNotification(member, reqaddr, text=text, lang=lang) # BAW: See the comment in MailList.py ChangeMemberAddress() for why we # set the Subject this way. del msg['subject'] msg['Subject'] = 'confirm ' + info.cookie msg.send(self) info.noticesleft -= 1 info.lastnotice = time.localtime()[:3] # In case the MemberAdaptor stores bounce info externally to # the list, we need to tell it to update self.setBounceInfo(member, info)
action += '.mbox' if parts[1:]: action = os.path.join(action, SLASH.join(parts[1:])) # If we added '/index.html' to true_filename, add a slash to the URL. # We need this because we no longer add the trailing slash in the # private.html template. It's always OK to test parts[-1] since we've # already verified parts[0] is listname. The basic rule is if the # post URL (action) is a directory, it must be slash terminated, but # not if it's a file. Otherwise, relative links in the target archive # page don't work. if true_filename.endswith('/index.html') and parts[-1] <> 'index.html': action += SLASH # Escape web input parameter to avoid cross-site scripting. print Utils.maketext('private.html', { 'action': Utils.websafe(action), 'realname': mlist.real_name, 'message': message, }, mlist=mlist) return lang = mlist.getMemberLanguage(username) i18n.set_language(lang) doc.set_language(lang) # Authorization confirmed... output the desired file try: ctype, enc = guess_type(path, strict=0) if ctype is None: ctype = 'text/html' if mboxfile: f = open(
def HandleBouncingAddress(self, addr, msg): """Disable or remove addr according to bounce_action setting.""" if self.automatic_bounce_action == 0: return elif self.automatic_bounce_action == 1: # Only send if call works ok. (succeeded, send) = self.DisableBouncingAddress(addr) did = "disabled" elif self.automatic_bounce_action == 2: (succeeded, send) = self.DisableBouncingAddress(addr) did = "disabled" # Never send. send = 0 elif self.automatic_bounce_action == 3: (succeeded, send) = self.RemoveBouncingAddress(addr) # Always send. send = 1 did = "removed" if send: if succeeded != 1: negative="not " else: negative="" recipient = self.GetAdminEmail() if addr in self.owner + [recipient]: # Whoops! This is a bounce of a bounce notice - do not # perpetuate the bounce loop! Log it prominently and be # satisfied with that. syslog("error", "%s: Bounce recipient loop" " encountered!\n\t%s\n\tBad admin recipient: %s" % (self.internal_name(), "(Ie, bounce notification addr, itself, bounces.)", addr)) return import mimetools boundary = mimetools.choose_boundary() # report about success but = '' if succeeded <> 1: but = 'BUT: %s' % succeeded # disabled? if did == 'disabled' and succeeded == 1: reenable = Utils.maketext( 'reenable.txt', {'admin_url': self.GetScriptURL('admin', absolute=1), }) else: reenable = '' # the mail message text text = Utils.maketext( 'bounce.txt', {'boundary' : boundary, 'listname' : self.real_name, 'addr' : addr, 'negative' : negative, 'did' : did, 'but' : but, 'reenable' : reenable, 'owneraddr': mm_cfg.MAILMAN_OWNER, }) # add this here so it doesn't get wrapped/filled text = text + '\n\n--' + boundary + \ '\nContent-type: text/plain; charset=us-ascii\n' # We do this here so this text won't be wrapped. note that # 'bounce.txt' has a trailing newline. Be robust about getting # the body of the message. body = '[original message unavailable]' try: body = msg.body except AttributeError: try: msg.rewindbody() body = msg.fp.read() except IOError: pass text = text + \ string.join(msg.headers, '') + '\n' + \ Utils.QuotePeriods(body) + '\n' + \ '--' + boundary + '--' if negative: negative = string.upper(negative) # send the bounce message msg = Message.UserNotification( recipient, mm_cfg.MAILMAN_OWNER, '%s member %s bouncing - %s%s' % (self.real_name, addr, negative, did), text) msg['MIME-Version'] = '1.0' msg['Content-Type'] = 'multipart/mixed; boundary="%s"' % boundary HandlerAPI.DeliverToUser(self, msg)
def main(): global ssort # Figure out which list is being requested parts = Utils.GetPathPieces() if not parts: handle_no_list() return listname = parts[0].lower() try: mlist = MailList.MailList(listname, lock=0) except Errors.MMListError as e: # Avoid cross-site scripting attacks safelistname = Utils.websafe(listname) # Send this with a 404 status. print('Status: 404 Not Found') handle_no_list(_('No such list <em>%(safelistname)s</em>')) syslog('error', 'admindb: No such list "%s": %s\n', listname, e) return # Now that we know which list to use, set the system's language to it. i18n.set_language(mlist.preferred_language) # Make sure the user is authorized to see this page. cgidata = cgi.FieldStorage(keep_blank_values=1) try: cgidata.getfirst('adminpw', '') except TypeError: # Someone crafted a POST with a bad Content-Type:. doc = Document() doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) doc.AddItem(Header(2, _("Error"))) doc.AddItem(Bold(_('Invalid options to CGI script.'))) # Send this with a 400 status. print('Status: 400 Bad Request') print(doc.Format()) return # CSRF check safe_params = ['adminpw', 'admlogin', 'msgid', 'sender', 'details'] params = list(cgidata.keys()) if set(params) - set(safe_params): csrf_checked = csrf_check(mlist, cgidata.getfirst('csrf_token')) else: csrf_checked = True # if password is present, void cookie to force password authentication. if cgidata.getfirst('adminpw'): os.environ['HTTP_COOKIE'] = '' csrf_checked = True if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin, mm_cfg.AuthListModerator, mm_cfg.AuthSiteAdmin), cgidata.getfirst('adminpw', '')): if 'adminpw' in cgidata: # This is a re-authorization attempt msg = Bold(FontSize('+1', _('Authorization failed.'))).Format() remote = os.environ.get('HTTP_FORWARDED_FOR', os.environ.get('HTTP_X_FORWARDED_FOR', os.environ.get('REMOTE_ADDR', 'unidentified origin'))) syslog('security', 'Authorization failed (admindb): list=%s: remote=%s', listname, remote) else: msg = '' Auth.loginpage(mlist, 'admindb', msg=msg) return # Add logout function. Note that admindb may be accessed with # site-wide admin, moderator and list admin privileges. # site admin may have site or admin cookie. (or both?) # See if this is a logout request if len(parts) >= 2 and parts[1] == 'logout': if mlist.AuthContextInfo(mm_cfg.AuthSiteAdmin)[0] == 'site': print(mlist.ZapCookie(mm_cfg.AuthSiteAdmin)) if mlist.AuthContextInfo(mm_cfg.AuthListModerator)[0]: print(mlist.ZapCookie(mm_cfg.AuthListModerator)) print(mlist.ZapCookie(mm_cfg.AuthListAdmin)) Auth.loginpage(mlist, 'admindb', frontpage=1) return # Set up the results document doc = Document() doc.set_language(mlist.preferred_language) # See if we're requesting all the messages for a particular sender, or if # we want a specific held message. sender = None msgid = None details = None envar = os.environ.get('QUERY_STRING') if envar: # POST methods, even if their actions have a query string, don't get # put into FieldStorage's keys :-( qs = cgi.parse_qs(envar).get('sender') if qs and isinstance(qs, list): sender = qs[0] qs = cgi.parse_qs(envar).get('msgid') if qs and isinstance(qs,list): msgid = qs[0] qs = cgi.parse_qs(envar).get('details') if qs and isinstance(qs, list): details = qs[0] # We need a signal handler to catch the SIGTERM that can come from Apache # when the user hits the browser's STOP button. See the comment in # admin.py for details. # # BAW: Strictly speaking, the list should not need to be locked just to # read the request database. However the request database asserts that # the list is locked in order to load it and it's not worth complicating # that logic. def sigterm_handler(signum, frame, mlist=mlist): # Make sure the list gets unlocked... mlist.Unlock() # ...and ensure we exit, otherwise race conditions could cause us to # enter MailList.Save() while we're in the unlocked state, and that # could be bad! sys.exit(0) mlist.Lock() try: # Install the emergency shutdown signal handler signal.signal(signal.SIGTERM, sigterm_handler) realname = mlist.real_name if not list(cgidata.keys()) or 'admlogin' in cgidata: # If this is not a form submission (i.e. there are no keys in the # form) or it's a login, then we don't need to do much special. doc.SetTitle(_('%(realname)s Administrative Database')) elif not details: # This is a form submission doc.SetTitle(_('%(realname)s Administrative Database Results')) if csrf_checked: process_form(mlist, doc, cgidata) else: doc.addError( _('The form lifetime has expired. (request forgery check)')) # Now print the results and we're done. Short circuit for when there # are no pending requests, but be sure to save the results! admindburl = mlist.GetScriptURL('admindb', absolute=1) if not mlist.NumRequestsPending(): title = _('%(realname)s Administrative Database') doc.SetTitle(title) doc.AddItem(Header(2, title)) doc.AddItem(_('There are no pending requests.')) doc.AddItem(' ') doc.AddItem(Link(admindburl, _('Click here to reload this page.'))) # Put 'Logout' link before the footer doc.AddItem('\n<div align="right"><font size="+2">') doc.AddItem(Link('%s/logout' % admindburl, '<b>%s</b>' % _('Logout'))) doc.AddItem('</font></div>\n') doc.AddItem(mlist.GetMailmanFooter()) print(doc.Format()) mlist.Save() return form = Form(admindburl, mlist=mlist, contexts=AUTH_CONTEXTS) # Add the instructions template if details == 'instructions': doc.AddItem(Header( 2, _('Detailed instructions for the administrative database'))) else: doc.AddItem(Header( 2, _('Administrative requests for mailing list:') + ' <em>%s</em>' % mlist.real_name)) if details != 'instructions': form.AddItem(Center(SubmitButton('submit', _('Submit All Data')))) nomessages = not mlist.GetHeldMessageIds() if not (details or sender or msgid or nomessages): form.AddItem(Center( '<label>' + CheckBox('discardalldefersp', 0).Format() + ' ' + _('Discard all messages marked <em>Defer</em>') + '</label>' )) # Add a link back to the overview, if we're not viewing the overview! adminurl = mlist.GetScriptURL('admin', absolute=1) d = {'listname' : mlist.real_name, 'detailsurl': admindburl + '?details=instructions', 'summaryurl': admindburl, 'viewallurl': admindburl + '?details=all', 'adminurl' : adminurl, 'filterurl' : adminurl + '/privacy/sender', } addform = 1 if sender: esender = Utils.websafe(sender) d['description'] = _("all of %(esender)s's held messages.") doc.AddItem(Utils.maketext('admindbpreamble.html', d, raw=1, mlist=mlist)) show_sender_requests(mlist, form, sender) elif msgid: d['description'] = _('a single held message.') doc.AddItem(Utils.maketext('admindbpreamble.html', d, raw=1, mlist=mlist)) show_message_requests(mlist, form, msgid) elif details == 'all': d['description'] = _('all held messages.') doc.AddItem(Utils.maketext('admindbpreamble.html', d, raw=1, mlist=mlist)) show_detailed_requests(mlist, form) elif details == 'instructions': doc.AddItem(Utils.maketext('admindbdetails.html', d, raw=1, mlist=mlist)) addform = 0 else: # Show a summary of all requests doc.AddItem(Utils.maketext('admindbsummary.html', d, raw=1, mlist=mlist)) num = show_pending_subs(mlist, form) num += show_pending_unsubs(mlist, form) num += show_helds_overview(mlist, form, ssort) addform = num > 0 # Finish up the document, adding buttons to the form if addform: doc.AddItem(form) form.AddItem('<hr>') if not (details or sender or msgid or nomessages): form.AddItem(Center( '<label>' + CheckBox('discardalldefersp', 0).Format() + ' ' + _('Discard all messages marked <em>Defer</em>') + '</label>' )) form.AddItem(Center(SubmitButton('submit', _('Submit All Data')))) # Put 'Logout' link before the footer doc.AddItem('\n<div align="right"><font size="+2">') doc.AddItem(Link('%s/logout' % admindburl, '<b>%s</b>' % _('Logout'))) doc.AddItem('</font></div>\n') doc.AddItem(mlist.GetMailmanFooter()) print(doc.Format()) # Commit all changes mlist.Save() finally: mlist.Unlock()