diff options
Diffstat (limited to 'Mailman')
-rw-r--r-- | Mailman/Archiver/HyperArch.py | 6 | ||||
-rw-r--r-- | Mailman/Cgi/confirm.py | 7 | ||||
-rw-r--r-- | Mailman/Cgi/options.py | 32 | ||||
-rw-r--r-- | Mailman/Commands/cmd_confirm.py | 8 | ||||
-rwxr-xr-x | Mailman/Defaults.py.in | 4 | ||||
-rw-r--r-- | Mailman/Gui/Privacy.py | 11 | ||||
-rw-r--r-- | Mailman/Gui/Topics.py | 10 | ||||
-rw-r--r-- | Mailman/HTMLFormatter.py | 5 | ||||
-rw-r--r-- | Mailman/Handlers/Moderate.py | 9 | ||||
-rw-r--r-- | Mailman/Handlers/Tagger.py | 6 | ||||
-rwxr-xr-x | Mailman/MailList.py | 81 | ||||
-rw-r--r-- | Mailman/Utils.py | 58 | ||||
-rw-r--r-- | Mailman/Version.py | 4 | ||||
-rwxr-xr-x | Mailman/versions.py | 13 |
14 files changed, 199 insertions, 55 deletions
diff --git a/Mailman/Archiver/HyperArch.py b/Mailman/Archiver/HyperArch.py index 9b1df75a..0c0e3356 100644 --- a/Mailman/Archiver/HyperArch.py +++ b/Mailman/Archiver/HyperArch.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -509,7 +509,7 @@ class Article(pipermail.Article): subject = self._get_subject_enc(self.prev) prev = ('<LINK REL="Previous" HREF="%s">' % (url_quote(self.prev.filename))) - prev_wsubj = ('<LI>' + _('Previous message:') + + prev_wsubj = ('<LI>' + _('Previous message (by thread):') + ' <A HREF="%s">%s\n</A></li>' % (url_quote(self.prev.filename), self.quote(subject))) @@ -531,7 +531,7 @@ class Article(pipermail.Article): subject = self._get_subject_enc(self.next) next = ('<LINK REL="Next" HREF="%s">' % (url_quote(self.next.filename))) - next_wsubj = ('<LI>' + _('Next message:') + + next_wsubj = ('<LI>' + _('Next message (by thread):') + ' <A HREF="%s">%s\n</A></li>' % (url_quote(self.next.filename), self.quote(subject))) diff --git a/Mailman/Cgi/confirm.py b/Mailman/Cgi/confirm.py index bb529318..97297e10 100644 --- a/Mailman/Cgi/confirm.py +++ b/Mailman/Cgi/confirm.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2014 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -99,8 +99,9 @@ def main(): %(safecookie)s. <p>Note that confirmation strings expire approximately - %(days)s days after the initial subscription request. If your - confirmation has expired, please try to re-submit your subscription. + %(days)s days after the initial request. They also expire if the + request has already been handled in some way. If your confirmation + has expired, please try to re-submit your request. Otherwise, <a href="%(confirmurl)s">re-enter</a> your confirmation string.''') diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py index 69ac52a9..74f186d7 100644 --- a/Mailman/Cgi/options.py +++ b/Mailman/Cgi/options.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -33,6 +33,7 @@ from Mailman import i18n from Mailman.htmlformat import * from Mailman.Logging.Syslog import syslog +OR = '|' SLASH = '/' SETLANGUAGE = -1 @@ -176,6 +177,9 @@ def main(): return # Are we processing an unsubscription request from the login screen? + msgc = _('If you are a list member, a confirmation email has been sent.') + msga = _("""If you are a list member, your unsubscription request has been + forwarded to the list administrator for approval.""") if cgidata.has_key('login-unsub'): # Because they can't supply a password for unsubscribing, we'll need # to do the confirmation dance. @@ -187,14 +191,11 @@ def main(): # be held. Otherwise, send a confirmation. if mlist.unsubscribe_policy: mlist.HoldUnsubscription(user) - doc.addError(_("""Your unsubscription request has been - forwarded to the list administrator for approval."""), - tag='') + doc.addError(msga, tag='') else: ip = os.environ.get('REMOTE_ADDR') mlist.ConfirmUnsubscription(user, userlang, remote=ip) - doc.addError(_('The confirmation email has been sent.'), - tag='') + doc.addError(msgc, tag='') mlist.Save() finally: mlist.Unlock() @@ -207,19 +208,21 @@ def main(): syslog('mischief', 'Unsub attempt of non-member w/ private rosters: %s', user) - doc.addError(_('The confirmation email has been sent.'), - tag='') + if mlist.unsubscribe_policy: + doc.addError(msga, tag='') + else: + doc.addError(msgc, tag='') loginpage(mlist, doc, user, language) print doc.Format() return # Are we processing a password reminder from the login screen? + msg = _("""If you are a list member, + your password has been emailed to you.""") if cgidata.has_key('login-remind'): if mlist.isMember(user): mlist.MailUserPassword(user) - doc.addError( - _('A reminder of your password has been emailed to you.'), - tag='') + doc.addError(msg, tag='') else: # Not a member if mlist.private_roster == 0: @@ -229,9 +232,7 @@ def main(): syslog('mischief', 'Reminder attempt of non-member w/ private rosters: %s', user) - doc.addError( - _('A reminder of your password has been emailed to you.'), - tag='') + doc.addError(msg, tag='') loginpage(mlist, doc, user, language) print doc.Format() return @@ -1068,7 +1069,8 @@ def topic_details(mlist, doc, user, cpuser, userlang, varhelp): table.AddRow([Bold(Label(_('Name:'))), Utils.websafe(name)]) table.AddRow([Bold(Label(_('Pattern (as regexp):'))), - '<pre>' + Utils.websafe(pattern) + '</pre>']) + '<pre>' + Utils.websafe(OR.join(pattern.splitlines())) + + '</pre>']) table.AddRow([Bold(Label(_('Description:'))), Utils.websafe(description)]) # Make colors look nice diff --git a/Mailman/Commands/cmd_confirm.py b/Mailman/Commands/cmd_confirm.py index a3accf64..379d23c2 100644 --- a/Mailman/Commands/cmd_confirm.py +++ b/Mailman/Commands/cmd_confirm.py @@ -1,4 +1,4 @@ -# Copyright (C) 2002-2011 by the Free Software Foundation, Inc. +# Copyright (C) 2002-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -49,9 +49,9 @@ def process(res, args): days = int(mm_cfg.PENDING_REQUEST_LIFE / mm_cfg.days(1) + 0.5) res.results.append(_("""\ Invalid confirmation string. Note that confirmation strings expire -approximately %(days)s days after the initial subscription request. If your -confirmation has expired, please try to re-submit your original request or -message.""")) +approximately %(days)s days after the initial request. They also expire if +the request has already been handled in some way. If your confirmation has +expired, please try to re-submit your original request or message.""")) except Errors.MMNeedApproval: res.results.append(_("""\ Your request has been forwarded to the list moderator for approval.""")) diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in index bec3e441..8a5e6b0e 100755 --- a/Mailman/Defaults.py.in +++ b/Mailman/Defaults.py.in @@ -1188,6 +1188,10 @@ DEFAULT_SUBSCRIBE_POLICY = 1 # Does this site allow completely unchecked subscriptions? ALLOW_OPEN_SUBSCRIBE = No +# This is the default list of addresses and regular expressions (beginning +# with ^) that are exempt from approval if SUBSCRIBE_POLICY is 2 or 3. +DEFAULT_SUBSCRIBE_AUTO_APPROVAL = [] + # The default policy for unsubscriptions. 0 (unmoderated unsubscribes) is # highly recommended! # 0 - unmoderated unsubscribes diff --git a/Mailman/Gui/Privacy.py b/Mailman/Gui/Privacy.py index 7f1e12f3..3c32bf50 100644 --- a/Mailman/Gui/Privacy.py +++ b/Mailman/Gui/Privacy.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2014 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -113,6 +113,15 @@ class Privacy(GUIBase): sub_cfentry, + ('subscribe_auto_approval', mm_cfg.EmailListEx, (10, WIDTH), 1, + _("""List of addresses (or regexps) whose subscriptions do not + require approval."""), + + _("""When subscription requires approval, addresses in this list + are allowed to subscribe without administrator approval. Add + addresses one per line. You may begin a line with a ^ character + to designate a (case insensitive) regular expression match.""")), + ('unsubscribe_policy', mm_cfg.Radio, (_('No'), _('Yes')), 0, _("""Is the list moderator's approval required for unsubscription requests? (<em>No</em> is recommended)"""), diff --git a/Mailman/Gui/Topics.py b/Mailman/Gui/Topics.py index 96f9b421..ec60dbda 100644 --- a/Mailman/Gui/Topics.py +++ b/Mailman/Gui/Topics.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -29,6 +29,8 @@ except NameError: True = 1 False = 0 +OR = '|' + class Topics(GUIBase): @@ -126,10 +128,10 @@ class Topics(GUIBase): # Make sure the pattern was a legal regular expression name = Utils.websafe(name) try: - # Tagger compiles in verbose mode so we do too. - re.compile(pattern, re.VERBOSE) + orpattern = OR.join(pattern.splitlines()) + re.compile(orpattern) except (re.error, TypeError): - safepattern = Utils.websafe(pattern) + safepattern = Utils.websafe(orpattern) doc.addError(_("""The topic pattern '%(safepattern)s' is not a legal regular expression. It will be discarded.""")) continue diff --git a/Mailman/HTMLFormatter.py b/Mailman/HTMLFormatter.py index dad51e74..df22e5f2 100644 --- a/Mailman/HTMLFormatter.py +++ b/Mailman/HTMLFormatter.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2010 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -90,6 +90,9 @@ class HTMLFormatter: showing = Utils.ObscureEmail(person, for_text=1) else: showing = person + realname = Utils.uncanonstr(self.getMemberName(person), lang) + if realname: + showing += " (%s)" % Utils.websafe(realname) got = Link(url, showing) if self.getDeliveryStatus(person) <> MemberAdaptor.ENABLED: got = Italic('(', got, ')') diff --git a/Mailman/Handlers/Moderate.py b/Mailman/Handlers/Moderate.py index 4400d086..225ee37f 100644 --- a/Mailman/Handlers/Moderate.py +++ b/Mailman/Handlers/Moderate.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2014 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -162,9 +162,10 @@ def do_reject(mlist): Utils.wrap(_(mlist.nonmember_rejection_notice)) else: raise Errors.RejectMessage, Utils.wrap(_("""\ -You are not allowed to post to this mailing list, and your message has been -automatically rejected. If you think that your messages are being rejected in -error, contact the mailing list owner at %(listowner)s.""")) +Your message has been rejected, probably because you are not subscribed to the +mailing list and the list's policy is to prohibit non-members from posting to +it. If you think that your messages are being rejected in error, contact the +mailing list owner at %(listowner)s.""")) diff --git a/Mailman/Handlers/Tagger.py b/Mailman/Handlers/Tagger.py index cb90bfc4..ed9a7e71 100644 --- a/Mailman/Handlers/Tagger.py +++ b/Mailman/Handlers/Tagger.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2013 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -29,6 +29,7 @@ from Mailman import Utils from Mailman.Logging.Syslog import syslog from Mailman.Handlers.CookHeaders import change_header +OR = '|' CRNL = '\r\n' EMPTYSTRING = '' NLTAB = '\n\t' @@ -63,7 +64,8 @@ def process(mlist, msg, msgdata): # added to the specific topics bucket. hits = {} for name, pattern, desc, emptyflag in mlist.topics: - cre = re.compile(pattern, re.IGNORECASE | re.VERBOSE) + pattern = OR.join(pattern.splitlines()) + cre = re.compile(pattern, re.IGNORECASE) for line in matchlines: if cre.search(line): hits[name] = 1 diff --git a/Mailman/MailList.py b/Mailman/MailList.py index 0ebf8ab8..dca8c8f5 100755 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -74,6 +74,7 @@ from Mailman.Logging.Syslog import syslog _ = i18n._ EMPTYSTRING = '' +OR = '|' try: True, False @@ -356,6 +357,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, self.welcome_msg = '' self.goodbye_msg = '' self.subscribe_policy = mm_cfg.DEFAULT_SUBSCRIBE_POLICY + self.subscribe_auto_approval = mm_cfg.DEFAULT_SUBSCRIBE_AUTO_APPROVAL self.unsubscribe_policy = mm_cfg.DEFAULT_UNSUBSCRIBE_POLICY self.private_roster = mm_cfg.DEFAULT_PRIVATE_ROSTER self.obscure_addresses = mm_cfg.DEFAULT_OBSCURE_ADDRESSES @@ -773,10 +775,11 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, goodtopics = [] for name, pattern, desc, emptyflag in self.topics: try: - re.compile(pattern) + orpattern = OR.join(pattern.splitlines()) + re.compile(orpattern) except (re.error, TypeError): syslog('error', 'Bad topic pattern "%s" for list: %s', - pattern, self.internal_name()) + orpattern, self.internal_name()) else: goodtopics.append((name, pattern, desc, emptyflag)) self.topics = goodtopics @@ -941,6 +944,9 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, syslog('subscribe', '%s: pending %s %s', self.internal_name(), who, by) raise Errors.MMSubscribeNeedsConfirmation + elif self.HasAutoApprovedSender(email): + # no approval necessary: + self.ApprovedAddMember(userdesc) else: # Subscription approval is required. Add this entry to the admin # requests database. BAW: this should probably take a userdesc @@ -1164,12 +1170,14 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # CP address of a member, then if the old address yields a different # CP address, we can simply remove the old address, otherwise we can # do nothing. + cpoldaddr = self.getMemberCPAddress(oldaddr) if self.isMember(newaddr) and (self.getMemberCPAddress(newaddr) == newaddr): - if self.getMemberCPAddress(oldaddr) <> newaddr: + if cpoldaddr <> newaddr: self.removeMember(oldaddr) else: self.changeMemberAddress(oldaddr, newaddr) + self.log_and_notify_admin(cpoldaddr, newaddr) # If globally is true, then we also include every list for which # oldaddr is a member. if not globally: @@ -1189,16 +1197,46 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, mlist.Lock() try: # Same logic as above, re newaddr is already a member + cpoldaddr = mlist.getMemberCPAddress(oldaddr) if mlist.isMember(newaddr) and ( mlist.getMemberCPAddress(newaddr) == newaddr): - if mlist.getMemberCPAddress(oldaddr) <> newaddr: + if cpoldaddr <> newaddr: mlist.removeMember(oldaddr) else: mlist.changeMemberAddress(oldaddr, newaddr) + mlist.log_and_notify_admin(cpoldaddr, newaddr) mlist.Save() finally: mlist.Unlock() + def log_and_notify_admin(self, oldaddr, newaddr): + """Log member address change and notify admin if requested.""" + syslog('subscribe', '%s: changed member address from %s to %s', + self.internal_name(), oldaddr, newaddr) + if self.admin_notify_mchanges: + lang = self.preferred_language + otrans = i18n.get_translation() + i18n.set_language(lang) + try: + realname = self.real_name + subject = _('%(realname)s address change notification') + finally: + i18n.set_translation(otrans) + name = self.getMemberName(newaddr) + if name is None: + name = '' + if isinstance(name, UnicodeType): + name = name.encode(Utils.GetCharSet(lang), 'replace') + text = Utils.maketext( + 'adminaddrchgack.txt', + {'name' : name, + 'oldaddr' : oldaddr, + 'newaddr' : newaddr, + 'listname': self.real_name, + }, mlist=self) + msg = Message.OwnerNotification(self, subject, text) + msg.send(self) + # # Confirmation processing @@ -1242,7 +1280,8 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # list administrators. self.SendHostileSubscriptionNotice(invitation, addr) raise Errors.HostileSubscriptionError - elif self.subscribe_policy in (2, 3): + elif self.subscribe_policy in (2, 3) and \ + not self.HasAutoApprovedSender(addr): self.HoldSubscription(addr, fullname, password, digest, lang) name = self.real_name raise Errors.MMNeedApproval, _( @@ -1521,13 +1560,30 @@ bad regexp in bounce_matching_header line: %s """Returns matched entry in ban_list if email matches. Otherwise returns None. """ - ban = False - for pattern in self.ban_list: + return self.GetPattern(email, self.ban_list) + + def HasAutoApprovedSender(self, sender): + """Returns True and logs if sender matches address or pattern + in subscribe_auto_approval. Otherwise returns False. + """ + auto_approve = False + if self.GetPattern(sender, self.subscribe_auto_approval): + auto_approve = True + syslog('vette', '%s: auto approved subscribe from %s', + self.internal_name(), sender) + return auto_approve + + def GetPattern(self, email, pattern_list): + """Returns matched entry in pattern_list if email matches. + Otherwise returns None. + """ + matched = None + for pattern in pattern_list: if pattern.startswith('^'): # This is a regular expression match try: if re.search(pattern, email, re.IGNORECASE): - ban = True + matched = pattern break except re.error: # BAW: we should probably remove this pattern @@ -1535,12 +1591,9 @@ bad regexp in bounce_matching_header line: %s else: # Do the comparison case insensitively if pattern.lower() == email.lower(): - ban = True + matched = pattern break - if ban: - return pattern - else: - return None + return matched diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 1a08c119..0cb9f122 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -80,6 +80,7 @@ except ImportError: EMPTYSTRING = '' UEMPTYSTRING = u'' +CR = '\r' NL = '\n' DOT = '.' IDENTCHARS = ascii_letters + digits + '_' @@ -918,6 +919,61 @@ def oneline(s, cset): return EMPTYSTRING.join(s.splitlines()) +def strip_verbose_pattern(pattern): + # Remove white space and comments from a verbose pattern and return a + # non-verbose, equivalent pattern. Replace CR and NL in the result + # with '\\r' and '\\n' respectively to avoid multi-line results. + if not isinstance(pattern, str): + return pattern + newpattern = '' + i = 0 + inclass = False + skiptoeol = False + copynext = False + while i < len(pattern): + c = pattern[i] + if copynext: + if c == NL: + newpattern += '\\n' + elif c == CR: + newpattern += '\\r' + else: + newpattern += c + copynext = False + elif skiptoeol: + if c == NL: + skiptoeol = False + elif c == '#' and not inclass: + skiptoeol = True + elif c == '[' and not inclass: + inclass = True + newpattern += c + copynext = True + elif c == ']' and inclass: + inclass = False + newpattern += c + elif re.search('\s', c): + if inclass: + if c == NL: + newpattern += '\\n' + elif c == CR: + newpattern += '\\r' + else: + newpattern += c + elif c == '\\' and not inclass: + newpattern += c + copynext = True + else: + if c == NL: + newpattern += '\\n' + elif c == CR: + newpattern += '\\r' + else: + newpattern += c + i += 1 + return newpattern + + # Patterns and functions to flag possible XSS attacks in HTML. # This list is compiled from information at http://ha.ckers.org/xss.html, # http://www.quirksmode.org/js/events_compinfo.html, diff --git a/Mailman/Version.py b/Mailman/Version.py index 66eed767..93616e5f 100644 --- a/Mailman/Version.py +++ b/Mailman/Version.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -37,7 +37,7 @@ HEX_VERSION = ((MAJOR_REV << 24) | (MINOR_REV << 16) | (MICRO_REV << 8) | (REL_LEVEL << 4) | (REL_SERIAL << 0)) # config.pck schema version number -DATA_FILE_VERSION = 105 +DATA_FILE_VERSION = 106 # qfile/*.db schema version number QFILE_SCHEMA_VERSION = 3 diff --git a/Mailman/versions.py b/Mailman/versions.py index 9006ec0c..d0960e0d 100755 --- a/Mailman/versions.py +++ b/Mailman/versions.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -313,6 +313,15 @@ def UpdateOldVars(l, stored_state): pass else: l.digest_members[k] = 0 + # + # Convert pre 2.2 topics regexps which were compiled in verbose mode + # to a non-verbose equivalent. + # + if stored_state['data_version'] < 106 and stored_state.has_key('topics'): + l.topics = [] + for name, pattern, description, emptyflag in stored_state['topics']: + pattern = Utils.strip_verbose_pattern(pattern) + l.topics.append((name, pattern, description, emptyflag)) # from_is_list was called author_is_list in 2.1.16rc2 (only). PreferStored('author_is_list', 'from_is_list', mm_cfg.DEFAULT_FROM_IS_LIST) @@ -352,6 +361,8 @@ def NewVars(l): add_only_if_missing('personalize', 0) add_only_if_missing('first_strip_reply_to', mm_cfg.DEFAULT_FIRST_STRIP_REPLY_TO) + add_only_if_missing('subscribe_auto_approval', + mm_cfg.DEFAULT_SUBSCRIBE_AUTO_APPROVAL) add_only_if_missing('unsubscribe_policy', mm_cfg.DEFAULT_UNSUBSCRIBE_POLICY) add_only_if_missing('send_goodbye_msg', mm_cfg.DEFAULT_SEND_GOODBYE_MSG) |