"""The class representing a Mailman mailing list.
Mixes in many task-specific classes.
"""
import sys
import os
import time
import marshal
import errno
import re
import shutil
import socket
import urllib
import cPickle
from cStringIO import StringIO
from UserDict import UserDict
from urlparse import urlparse
from types import *
import email.Iterators
from email.Utils import getaddresses, formataddr, parseaddr
from email.Header import Header
from Mailman import mm_cfg
from Mailman import Utils
from Mailman import Errors
from Mailman import LockFile
from Mailman.UserDesc import UserDesc
from Mailman.Archiver import Archiver
from Mailman.Autoresponder import Autoresponder
from Mailman.Bouncer import Bouncer
from Mailman.Deliverer import Deliverer
from Mailman.Digester import Digester
from Mailman.GatewayManager import GatewayManager
from Mailman.HTMLFormatter import HTMLFormatter
from Mailman.ListAdmin import ListAdmin
from Mailman.SecurityManager import SecurityManager
from Mailman.TopicMgr import TopicMgr
from Mailman import Pending
from Mailman import Gui
from Mailman import MemberAdaptor
from Mailman.OldStyleMemberships import OldStyleMemberships
from Mailman import Message
from Mailman import Site
from Mailman import i18n
from Mailman.Logging.Syslog import syslog
_ = i18n._
EMPTYSTRING = ''
try:
True, False
except NameError:
True = 1
False = 0
class MailList(HTMLFormatter, Deliverer, ListAdmin,
Archiver, Digester, SecurityManager, Bouncer, GatewayManager,
Autoresponder, TopicMgr, Pending.Pending):
def __init__(self, name=None, lock=1):
for baseclass in self.__class__.__bases__:
if hasattr(baseclass, '__init__'):
baseclass.__init__(self)
self.InitTempVars(name)
self._memberadaptor = OldStyleMemberships(self)
if name is None:
return
filename = os.path.join(self.fullpath(), 'extend.py')
dict = {}
try:
execfile(filename, dict)
except IOError, e:
if e.errno == errno.ENOENT:
pass
else:
syslog('error', 'IOError reading list extension: %s', e)
else:
func = dict.get('extend')
if func:
func(self)
if lock:
self.Lock()
else:
self.Load()
def __getattr__(self, name):
try:
return getattr(self._memberadaptor, name)
except AttributeError:
for guicomponent in self._gui:
try:
return getattr(guicomponent, name)
except AttributeError:
pass
else:
raise AttributeError, name
def __repr__(self):
if self.Locked():
status = '(locked)'
else:
status = '(unlocked)'
return '<mailing list "%s" %s at %x>' % (
self.internal_name(), status, id(self))
def Lock(self, timeout=0):
self.__lock.lock(timeout)
try:
self.Load()
except Exception:
self.Unlock()
raise
def Unlock(self):
self.__lock.unlock(unconditionally=1)
def Locked(self):
return self.__lock.locked()
def internal_name(self):
return self._internal_name
def fullpath(self):
return self._full_path
def getListAddress(self, extra=None):
if extra is None:
return '%s@%s' % (self.internal_name(), self.host_name)
return '%s-%s@%s' % (self.internal_name(), extra, self.host_name)
def GetBouncesEmail(self):
return self.getListAddress('bounces')
def GetOwnerEmail(self):
return self.getListAddress('owner')
def GetRequestEmail(self, cookie=''):
if mm_cfg.VERP_CONFIRMATIONS and cookie:
return self.GetConfirmEmail(cookie)
else:
return self.getListAddress('request')
def GetConfirmEmail(self, cookie):
return mm_cfg.VERP_CONFIRM_FORMAT % {
'addr' : '%s-confirm' % self.internal_name(),
'cookie': cookie,
} + '@' + self.host_name
def GetConfirmJoinSubject(self, listname, cookie):
if mm_cfg.VERP_CONFIRMATIONS and cookie:
cset = i18n.get_translation().charset() or \
Utils.GetCharSet(self.preferred_language)
subj = Header(
_('Your confirmation is required to join the %(listname)s mailing list'),
cset, header_name='subject')
return subj
else:
return 'confirm ' + cookie
def GetConfirmLeaveSubject(self, listname, cookie):
if mm_cfg.VERP_CONFIRMATIONS and cookie:
cset = i18n.get_translation().charset() or \
Utils.GetCharSet(self.preferred_language)
subj = Header(
_('Your confirmation is required to leave the %(listname)s mailing list'),
cset, header_name='subject')
return subj
else:
return 'confirm ' + cookie
def GetListEmail(self):
return self.getListAddress()
def GetMemberAdminEmail(self, member):
"""Usually the member addr, but modified for umbrella lists.
Umbrella lists have other mailing lists as members, and so admin stuff
like confirmation requests and passwords must not be sent to the
member addresses - the sublists - but rather to the administrators of
the sublists. This routine picks the right address, considering
regular member address to be their own administrative addresses.
"""
if not self.umbrella_list:
return member
else:
acct, host = tuple(member.split('@'))
return "%s%s@%s" % (acct, self.umbrella_member_suffix, host)
def GetScriptURL(self, scriptname, absolute=0):
return Utils.ScriptURL(scriptname, self.web_page_url, absolute) + \
'/' + self.internal_name()
def GetOptionsURL(self, user, obscure=0, absolute=0):
url = self.GetScriptURL('options', absolute)
if obscure:
user = Utils.ObscureEmail(user)
return '%s/%s' % (url, urllib.quote(user.lower()))
def InitTempVars(self, name):
"""Set transient variables of this and inherited classes."""
self.__timestamp = 0
self.__lock = LockFile.LockFile(
os.path.join(mm_cfg.LOCK_DIR, name or '<site>') + '.lock',
lifetime = mm_cfg.LIST_LOCK_LIFETIME,
withlogging = mm_cfg.LIST_LOCK_DEBUGGING)
self._internal_name = name
if name:
self._full_path = Site.get_listpath(name)
else:
self._full_path = ''
for baseclass in self.__class__.__bases__:
if hasattr(baseclass, 'InitTempVars'):
baseclass.InitTempVars(self)
self._gui = []
for component in dir(Gui):
if component.startswith('_'):
continue
self._gui.append(getattr(Gui, component)())
def InitVars(self, name=None, admin='', crypted_password=''):
"""Assign default values - some will be overriden by stored state."""
if name:
self._internal_name = name
self.created_at = time.time()
self.volume = 1
self.members = {} self.data_version = mm_cfg.DATA_FILE_VERSION
self.last_post_time = 0
self.post_id = 1. self.user_options = {}
self.language = {}
self.usernames = {}
self.passwords = {}
self.new_member_options = mm_cfg.DEFAULT_NEW_MEMBER_OPTIONS
self.respond_to_post_requests = 1
self.advertised = mm_cfg.DEFAULT_LIST_ADVERTISED
self.max_num_recipients = mm_cfg.DEFAULT_MAX_NUM_RECIPIENTS
self.max_message_size = mm_cfg.DEFAULT_MAX_MESSAGE_SIZE
self.host_name = mm_cfg.DEFAULT_HOST_NAME or mm_cfg.DEFAULT_EMAIL_HOST
self.web_page_url = (
mm_cfg.DEFAULT_URL or
mm_cfg.DEFAULT_URL_PATTERN % mm_cfg.DEFAULT_URL_HOST)
self.owner = [admin]
self.moderator = []
self.reply_goes_to_list = mm_cfg.DEFAULT_REPLY_GOES_TO_LIST
self.reply_to_address = ''
self.first_strip_reply_to = mm_cfg.DEFAULT_FIRST_STRIP_REPLY_TO
self.admin_immed_notify = mm_cfg.DEFAULT_ADMIN_IMMED_NOTIFY
self.admin_notify_mchanges = \
mm_cfg.DEFAULT_ADMIN_NOTIFY_MCHANGES
self.require_explicit_destination = \
mm_cfg.DEFAULT_REQUIRE_EXPLICIT_DESTINATION
self.acceptable_aliases = mm_cfg.DEFAULT_ACCEPTABLE_ALIASES
self.umbrella_list = mm_cfg.DEFAULT_UMBRELLA_LIST
self.umbrella_member_suffix = \
mm_cfg.DEFAULT_UMBRELLA_MEMBER_ADMIN_SUFFIX
self.send_reminders = mm_cfg.DEFAULT_SEND_REMINDERS
self.send_welcome_msg = mm_cfg.DEFAULT_SEND_WELCOME_MSG
self.send_goodbye_msg = mm_cfg.DEFAULT_SEND_GOODBYE_MSG
self.bounce_matching_headers = \
mm_cfg.DEFAULT_BOUNCE_MATCHING_HEADERS
self.header_filter_rules = []
self.anonymous_list = mm_cfg.DEFAULT_ANONYMOUS_LIST
internalname = self.internal_name()
self.real_name = internalname[0].upper() + internalname[1:]
self.description = ''
self.info = ''
self.welcome_msg = ''
self.goodbye_msg = ''
self.subscribe_policy = mm_cfg.DEFAULT_SUBSCRIBE_POLICY
self.unsubscribe_policy = mm_cfg.DEFAULT_UNSUBSCRIBE_POLICY
self.private_roster = mm_cfg.DEFAULT_PRIVATE_ROSTER
self.obscure_addresses = mm_cfg.DEFAULT_OBSCURE_ADDRESSES
self.admin_member_chunksize = mm_cfg.DEFAULT_ADMIN_MEMBER_CHUNKSIZE
self.administrivia = mm_cfg.DEFAULT_ADMINISTRIVIA
self.preferred_language = mm_cfg.DEFAULT_SERVER_LANGUAGE
self.available_languages = []
self.include_rfc2369_headers = 1
self.include_list_post_header = 1
self.filter_mime_types = mm_cfg.DEFAULT_FILTER_MIME_TYPES
self.pass_mime_types = mm_cfg.DEFAULT_PASS_MIME_TYPES
self.filter_filename_extensions = \
mm_cfg.DEFAULT_FILTER_FILENAME_EXTENSIONS
self.pass_filename_extensions = mm_cfg.DEFAULT_PASS_FILENAME_EXTENSIONS
self.filter_content = mm_cfg.DEFAULT_FILTER_CONTENT
self.collapse_alternatives = mm_cfg.DEFAULT_COLLAPSE_ALTERNATIVES
self.convert_html_to_plaintext = \
mm_cfg.DEFAULT_CONVERT_HTML_TO_PLAINTEXT
self.filter_action = mm_cfg.DEFAULT_FILTER_ACTION
self.nondigestable = mm_cfg.DEFAULT_NONDIGESTABLE
self.personalize = 0
self.default_member_moderation = \
mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION
self.emergency = 0
self.member_moderation_action = 0
self.member_moderation_notice = ''
self.accept_these_nonmembers = []
self.hold_these_nonmembers = []
self.reject_these_nonmembers = []
self.discard_these_nonmembers = []
self.forward_auto_discards = mm_cfg.DEFAULT_FORWARD_AUTO_DISCARDS
self.generic_nonmember_action = mm_cfg.DEFAULT_GENERIC_NONMEMBER_ACTION
self.nonmember_rejection_notice = ''
self.ban_list = []
self.password = crypted_password
self.hold_and_cmd_autoresponses = {}
for baseclass in self.__class__.__bases__:
if hasattr(baseclass, 'InitVars'):
baseclass.InitVars(self)
self.subject_prefix = mm_cfg.DEFAULT_SUBJECT_PREFIX % self.__dict__
self.msg_header = mm_cfg.DEFAULT_MSG_HEADER
self.msg_footer = mm_cfg.DEFAULT_MSG_FOOTER
if Utils.GetCharSet(self.preferred_language) == 'us-ascii':
self.encode_ascii_prefixes = 0
else:
self.encode_ascii_prefixes = 2
self.scrub_nondigest = mm_cfg.DEFAULT_SCRUB_NONDIGEST
self.max_days_to_hold = mm_cfg.DEFAULT_MAX_DAYS_TO_HOLD
def GetConfigCategories(self):
class CategoryDict(UserDict):
def __init__(self):
UserDict.__init__(self)
self.keysinorder = mm_cfg.ADMIN_CATEGORIES[:]
def keys(self):
return self.keysinorder
def items(self):
items = []
for k in mm_cfg.ADMIN_CATEGORIES:
items.append((k, self.data[k]))
return items
def values(self):
values = []
for k in mm_cfg.ADMIN_CATEGORIES:
values.append(self.data[k])
return values
categories = CategoryDict()
for gui in self._gui:
k, v = gui.GetConfigCategory()
categories[k] = (v, gui)
return categories
def GetConfigSubCategories(self, category):
for gui in self._gui:
if hasattr(gui, 'GetConfigSubCategories'):
subcat = gui.GetConfigSubCategories(category)
if subcat is not None:
return subcat
return None
def GetConfigInfo(self, category, subcat=None):
for gui in self._gui:
if hasattr(gui, 'GetConfigInfo'):
value = gui.GetConfigInfo(self, category, subcat)
if value:
return value
def Create(self, name, admin, crypted_password,
langs=None, emailhost=None):
if Utils.list_exists(name):
raise Errors.MMListAlreadyExistsError, name
if emailhost is None:
emailhost = mm_cfg.DEFAULT_EMAIL_HOST
postingaddr = '%s@%s' % (name, emailhost)
try:
Utils.ValidateEmail(postingaddr)
except Errors.MMBadEmailError:
raise Errors.BadListNameError, postingaddr
Utils.ValidateEmail(admin)
self._internal_name = name
self._full_path = Site.get_listpath(name, create=1)
self.__lock.lock()
self.InitVars(name, admin, crypted_password)
self.CheckValues()
if langs is None:
self.available_languages = [self.preferred_language]
else:
self.available_languages = langs
def __save(self, dict):
fname = os.path.join(self.fullpath(), 'config.pck')
fname_tmp = fname + '.tmp.%s.%d' % (socket.gethostname(), os.getpid())
fname_last = fname + '.last'
fp = None
try:
fp = open(fname_tmp, 'w')
cPickle.dump(dict, fp, 1)
fp.flush()
if mm_cfg.SYNC_AFTER_WRITE:
os.fsync(fp.fileno())
fp.close()
except IOError, e:
syslog('error',
'Failed config.pck write, retaining old state.\n%s', e)
if fp is not None:
os.unlink(fname_tmp)
raise
try:
os.unlink(fname_last)
except OSError, e:
if e.errno <> errno.ENOENT: raise
try:
os.link(fname, fname_last)
except OSError, e:
if e.errno <> errno.ENOENT: raise
os.rename(fname_tmp, fname)
self.__timestamp = os.path.getmtime(fname)
def Save(self):
self.__lock.refresh()
dict = {}
for key, value in self.__dict__.items():
if key[0] == '_' or type(value) is MethodType:
continue
dict[key] = value
omask = os.umask(007)
try:
self.__save(dict)
finally:
os.umask(omask)
self.SaveRequestsDb()
self.CheckHTMLArchiveDir()
def __load(self, dbfile):
if dbfile.endswith('.db') or dbfile.endswith('.db.last'):
loadfunc = marshal.load
elif dbfile.endswith('.pck') or dbfile.endswith('.pck.last'):
loadfunc = cPickle.load
else:
assert 0, 'Bad database file name'
try:
mtime = os.path.getmtime(dbfile)
if mtime <= self.__timestamp:
return None, None
fp = open(dbfile)
except EnvironmentError, e:
if e.errno <> errno.ENOENT: raise
return None, e
try:
try:
dict = loadfunc(fp)
if type(dict) <> DictType:
return None, 'Load() expected to return a dictionary'
except (EOFError, ValueError, TypeError, MemoryError,
cPickle.PicklingError, cPickle.UnpicklingError), e:
return None, e
finally:
fp.close()
self.__timestamp = mtime
return dict, None
def Load(self, check_version=True):
if not Utils.list_exists(self.internal_name()):
raise Errors.MMUnknownListError
pfile = os.path.join(self.fullpath(), 'config.pck')
plast = pfile + '.last'
dfile = os.path.join(self.fullpath(), 'config.db')
dlast = dfile + '.last'
for file in (pfile, plast, dfile, dlast):
dict, e = self.__load(file)
if dict is None:
if e is not None:
syslog('error', "couldn't load config file %s\n%s",
file, e)
else:
return
else:
break
else:
syslog('error', 'All %s fallbacks were corrupt, giving up',
self.internal_name())
raise Errors.MMCorruptListDatabaseError, e
if file == plast or file == dlast:
syslog('error', 'fixing corrupt config file, using: %s', file)
unlock = True
try:
try:
self.__lock.lock()
except LockFile.AlreadyLockedError:
unlock = False
self.__fix_corrupt_pckfile(file, pfile, plast, dfile, dlast)
finally:
if unlock:
self.__lock.unlock()
self.__dict__.update(dict)
if check_version:
self.CheckVersion(dict)
self.CheckValues()
def __fix_corrupt_pckfile(self, file, pfile, plast, dfile, dlast):
if file == plast:
try:
os.rename(pfile, pfile + '.corrupt')
except OSError, e:
if e.errno <> errno.ENOENT: raise
try:
os.remove(pfile + '.safety')
except OSError, e:
if e.errno <> errno.ENOENT: raise
shutil.copy(file, pfile)
shutil.copy(file, pfile + '.safety')
elif file == dlast:
try:
os.rename(dfile, dfile + '.corrupt')
except OSError, e:
if e.errno <> errno.ENOENT: raise
try:
os.remove(dfile + '.safety')
except OSError, e:
if e.errno <> errno.ENOENT: raise
shutil.copy(file, dfile)
shutil.copy(file, dfile + '.safety')
def CheckVersion(self, stored_state):
"""Auto-update schema if necessary."""
if self.data_version >= mm_cfg.DATA_FILE_VERSION:
return
self.InitVars()
self.__timestamp = 0
self.Load(check_version=0)
waslocked = self.Locked()
if not waslocked:
self.Lock()
try:
from versions import Update
Update(self, stored_state)
self.data_version = mm_cfg.DATA_FILE_VERSION
self.Save()
finally:
if not waslocked:
self.Unlock()
def CheckValues(self):
"""Normalize selected values to known formats."""
if '' in urlparse(self.web_page_url)[:2]:
self.web_page_url = (
mm_cfg.DEFAULT_URL or
mm_cfg.DEFAULT_URL_PATTERN % mm_cfg.DEFAULT_URL_HOST)
if self.web_page_url and self.web_page_url[-1] <> '/':
self.web_page_url = self.web_page_url + '/'
try:
if self.reply_to_address.strip() and self.reply_goes_to_list:
Utils.ValidateEmail(self.reply_to_address)
except Errors.EmailAddressError:
syslog('error', 'Bad reply_to_address "%s" cleared for list: %s',
self.reply_to_address, self.internal_name())
self.reply_to_address = ''
self.reply_goes_to_list = 0
goodtopics = []
for name, pattern, desc, emptyflag in self.topics:
try:
re.compile(pattern)
except (re.error, TypeError):
syslog('error', 'Bad topic pattern "%s" for list: %s',
pattern, self.internal_name())
else:
goodtopics.append((name, pattern, desc, emptyflag))
self.topics = goodtopics
def InviteNewMember(self, userdesc, text=''):
"""Invite a new member to the list.
This is done by creating a subscription pending for the user, and then
crafting a message to the member informing them of the invitation.
"""
invitee = userdesc.address
Utils.ValidateEmail(invitee)
pattern = self.GetBannedPattern(invitee)
if pattern:
raise Errors.MembershipIsBanned, pattern
userdesc.invitation = self.internal_name()
cookie = self.pend_new(Pending.SUBSCRIPTION, userdesc)
requestaddr = self.getListAddress('request')
confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
cookie)
listname = self.real_name
text += Utils.maketext(
'invite.txt',
{'email' : invitee,
'listname' : listname,
'hostname' : self.host_name,
'confirmurl' : confirmurl,
'requestaddr': requestaddr,
'cookie' : cookie,
'listowner' : self.GetOwnerEmail(),
}, mlist=self)
sender = self.GetRequestEmail(cookie)
msg = Message.UserNotification(
invitee, sender,
text=text, lang=self.preferred_language)
subj = self.GetConfirmJoinSubject(listname, cookie)
del msg['subject']
msg['Subject'] = subj
msg.send(self)
def AddMember(self, userdesc, remote=None):
"""Front end to member subscription.
This method enforces subscription policy, validates values, sends
notifications, and any other grunt work involved in subscribing a
user. It eventually calls ApprovedAddMember() to do the actual work
of subscribing the user.
userdesc is an instance with the following public attributes:
address -- the unvalidated email address of the member
fullname -- the member's full name (i.e. John Smith)
digest -- a flag indicating whether the user wants digests or not
language -- the requested default language for the user
password -- the user's password
Other attributes may be defined later. Only address is required; the
others all have defaults (fullname='', digests=0, language=list's
preferred language, password=generated).
remote is a string which describes where this add request came from.
"""
assert self.Locked()
email = Utils.LCDomain(userdesc.address)
name = getattr(userdesc, 'fullname', '')
lang = getattr(userdesc, 'language', self.preferred_language)
digest = getattr(userdesc, 'digest', None)
password = getattr(userdesc, 'password', Utils.MakeRandomPassword())
if digest is None:
if self.nondigestable:
digest = 0
else:
digest = 1
Utils.ValidateEmail(email)
if self.isMember(email):
raise Errors.MMAlreadyAMember, email
if email.lower() == self.GetListEmail().lower():
raise Errors.MMBadEmailError
realname = self.real_name
pattern = self.GetBannedPattern(email)
if pattern:
syslog('vette', '%s banned subscription: %s (matched: %s)',
realname, email, pattern)
raise Errors.MembershipIsBanned, pattern
if digest and not self.digestable:
raise Errors.MMCantDigestError
elif not digest and not self.nondigestable:
raise Errors.MMMustDigestError
userdesc.address = email
userdesc.fullname = name
userdesc.digest = digest
userdesc.language = lang
userdesc.password = password
if self.subscribe_policy == 0:
self.ApprovedAddMember(userdesc, whence=remote or '')
elif self.subscribe_policy == 1 or self.subscribe_policy == 3:
cookie = self.pend_new(Pending.SUBSCRIPTION, userdesc)
if remote is None:
by = remote = ''
else:
by = ' ' + remote
remote = _(' from %(remote)s')
recipient = self.GetMemberAdminEmail(email)
confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
cookie)
text = Utils.maketext(
'verify.txt',
{'email' : email,
'listaddr' : self.GetListEmail(),
'listname' : realname,
'cookie' : cookie,
'requestaddr' : self.getListAddress('request'),
'remote' : remote,
'listadmin' : self.GetOwnerEmail(),
'confirmurl' : confirmurl,
}, lang=lang, mlist=self)
msg = Message.UserNotification(
recipient, self.GetRequestEmail(cookie),
text=text, lang=lang)
del msg['subject']
msg['Subject'] = self.GetConfirmJoinSubject(realname, cookie)
msg['Reply-To'] = self.GetRequestEmail(cookie)
msg.send(self)
who = formataddr((name, email))
syslog('subscribe', '%s: pending %s %s',
self.internal_name(), who, by)
raise Errors.MMSubscribeNeedsConfirmation
else:
self.HoldSubscription(email, name, password, digest, lang)
raise Errors.MMNeedApproval, _(
'subscriptions to %(realname)s require moderator approval')
def ApprovedAddMember(self, userdesc, ack=None, admin_notif=None, text='',
whence=''):
"""Add a member right now.
The member's subscription must be approved by what ever policy the
list enforces.
userdesc is as above in AddMember().
ack is a flag that specifies whether the user should get an
acknowledgement of their being subscribed. Default is to use the
list's default flag value.
admin_notif is a flag that specifies whether the list owner should get
an acknowledgement of this subscription. Default is to use the list's
default flag value.
"""
assert self.Locked()
if ack is None:
ack = self.send_welcome_msg
if admin_notif is None:
admin_notif = self.admin_notify_mchanges
email = Utils.LCDomain(userdesc.address)
name = getattr(userdesc, 'fullname', '')
lang = getattr(userdesc, 'language', self.preferred_language)
digest = getattr(userdesc, 'digest', None)
password = getattr(userdesc, 'password', Utils.MakeRandomPassword())
if digest is None:
if self.nondigestable:
digest = 0
else:
digest = 1
Utils.ValidateEmail(email)
if self.isMember(email):
raise Errors.MMAlreadyAMember, email
pattern = self.GetBannedPattern(email)
if pattern:
raise Errors.MembershipIsBanned, pattern
self.addNewMember(email, realname=name, digest=digest,
password=password, language=lang)
self.setMemberOption(email, mm_cfg.DisableMime,
1 - self.mime_is_default_digest)
self.setMemberOption(email, mm_cfg.Moderate,
self.default_member_moderation)
if digest:
kind = ' (digest)'
else:
kind = ''
syslog('subscribe', '%s: new%s %s, %s', self.internal_name(),
kind, formataddr((name, email)), whence)
if ack:
self.SendSubscribeAck(email, self.getMemberPassword(email),
digest, text)
if admin_notif:
lang = self.preferred_language
otrans = i18n.get_translation()
i18n.set_language(lang)
try:
realname = self.real_name
subject = _('%(realname)s subscription notification')
finally:
i18n.set_translation(otrans)
if isinstance(name, UnicodeType):
name = name.encode(Utils.GetCharSet(lang), 'replace')
text = Utils.maketext(
"adminsubscribeack.txt",
{"listname" : realname,
"member" : formataddr((name, email)),
}, mlist=self)
msg = Message.OwnerNotification(self, subject, text)
msg.send(self)
def DeleteMember(self, name, whence=None, admin_notif=None, userack=True):
realname, email = parseaddr(name)
if self.unsubscribe_policy == 0:
self.ApprovedDeleteMember(name, whence, admin_notif, userack)
else:
self.HoldUnsubscription(email)
raise Errors.MMNeedApproval, _(
'unsubscriptions require moderator approval')
def ApprovedDeleteMember(self, name, whence=None,
admin_notif=None, userack=None):
if userack is None:
userack = self.send_goodbye_msg
if admin_notif is None:
admin_notif = self.admin_notify_mchanges
fullname, emailaddr = parseaddr(name)
userlang = self.getMemberLanguage(emailaddr)
self.removeMember(emailaddr)
if userack:
self.SendUnsubscribeAck(emailaddr, userlang)
if admin_notif:
realname = self.real_name
subject = _('%(realname)s unsubscribe notification')
text = Utils.maketext(
'adminunsubscribeack.txt',
{'member' : name,
'listname': self.real_name,
}, mlist=self)
msg = Message.OwnerNotification(self, subject, text)
msg.send(self)
if whence:
whence = "; %s" % whence
else:
whence = ""
syslog('subscribe', '%s: deleted %s%s',
self.internal_name(), name, whence)
def ChangeMemberName(self, addr, name, globally):
self.setMemberName(addr, name)
if not globally:
return
for listname in Utils.list_names():
if listname == self.internal_name():
continue
mlist = MailList(listname, lock=0)
if mlist.host_name <> self.host_name:
continue
if not mlist.isMember(addr):
continue
mlist.Lock()
try:
mlist.setMemberName(addr, name)
mlist.Save()
finally:
mlist.Unlock()
def ChangeMemberAddress(self, oldaddr, newaddr, globally):
newaddr = Utils.LCDomain(newaddr)
Utils.ValidateEmail(newaddr)
if not globally and newaddr == oldaddr and self.isMember(newaddr):
raise Errors.MMAlreadyAMember
if newaddr == self.GetListEmail().lower():
raise Errors.MMBadEmailError
realname = self.real_name
pattern = self.GetBannedPattern(newaddr)
if pattern:
syslog('vette',
'%s banned address change: %s -> %s (matched: %s)',
realname, oldaddr, newaddr, pattern)
raise Errors.MembershipIsBanned, pattern
cookie = self.pend_new(Pending.CHANGE_OF_ADDRESS,
oldaddr, newaddr, globally)
confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
cookie)
lang = self.getMemberLanguage(oldaddr)
text = Utils.maketext(
'verify.txt',
{'email' : newaddr,
'listaddr' : self.GetListEmail(),
'listname' : realname,
'cookie' : cookie,
'requestaddr': self.getListAddress('request'),
'remote' : '',
'listadmin' : self.GetOwnerEmail(),
'confirmurl' : confirmurl,
}, lang=lang, mlist=self)
msg = Message.UserNotification(
newaddr, self.GetRequestEmail(cookie),
text=text, lang=lang)
del msg['subject']
msg['Subject'] = self.GetConfirmJoinSubject(realname, cookie)
msg['Reply-To'] = self.GetRequestEmail(cookie)
msg.send(self)
def ApprovedChangeMemberAddress(self, oldaddr, newaddr, globally):
pattern = self.GetBannedPattern(newaddr)
if pattern:
raise Errors.MembershipIsBanned, pattern
if self.getMemberCPAddress(oldaddr) == newaddr:
self.removeMember(oldaddr)
else:
self.changeMemberAddress(oldaddr, newaddr)
if not globally:
return
for listname in Utils.list_names():
if listname == self.internal_name():
continue
mlist = MailList(listname, lock=0)
if mlist.host_name <> self.host_name:
continue
if not mlist.isMember(oldaddr):
continue
if mlist.GetBannedPattern(newaddr):
continue
mlist.Lock()
try:
if mlist.getMemberCPAddress(oldaddr) == newaddr:
mlist.removeMember(oldaddr)
else:
mlist.changeMemberAddress(oldaddr, newaddr)
mlist.Save()
finally:
mlist.Unlock()
def ProcessConfirmation(self, cookie, context=None):
rec = self.pend_confirm(cookie)
if rec is None:
raise Errors.MMBadConfirmation, 'No cookie record for %s' % cookie
try:
op = rec[0]
data = rec[1:]
except ValueError:
raise Errors.MMBadConfirmation, 'op-less data %s' % (rec,)
if op == Pending.SUBSCRIPTION:
whence = 'via email confirmation'
try:
userdesc = data[0]
if isinstance(context, UserDesc):
userdesc += context
whence = 'via web confirmation'
addr = userdesc.address
fullname = userdesc.fullname
password = userdesc.password
digest = userdesc.digest
lang = userdesc.language
except ValueError:
raise Errors.MMBadConfirmation, 'bad subscr data %s' % (data,)
invitation = getattr(userdesc, 'invitation', False)
if invitation:
if invitation <> self.internal_name():
self.SendHostileSubscriptionNotice(invitation, addr)
raise Errors.HostileSubscriptionError
elif self.subscribe_policy in (2, 3):
self.HoldSubscription(addr, fullname, password, digest, lang)
name = self.real_name
raise Errors.MMNeedApproval, _(
'subscriptions to %(name)s require administrator approval')
self.ApprovedAddMember(userdesc, whence=whence)
return op, addr, password, digest, lang
elif op == Pending.UNSUBSCRIPTION:
addr = data[0]
if isinstance(context, Message.Message):
whence = 'email confirmation'
else:
whence = 'web confirmation'
self.ApprovedDeleteMember(addr, whence=whence)
return op, addr
elif op == Pending.CHANGE_OF_ADDRESS:
oldaddr, newaddr, globally = data
self.ApprovedChangeMemberAddress(oldaddr, newaddr, globally)
return op, oldaddr, newaddr
elif op == Pending.HELD_MESSAGE:
id = data[0]
approved = None
if isinstance(context, Message.Message):
approved = context.get('Approved', context.get('Approve'))
if not approved:
try:
subpart = list(email.Iterators.typed_subpart_iterator(
context, 'text', 'plain'))[0]
except IndexError:
subpart = None
if subpart:
s = StringIO(subpart.get_payload())
while True:
line = s.readline()
if not line:
break
if not line.strip():
continue
i = line.find(':')
if i > 0:
if (line[:i].lower() == 'approve' or
line[:i].lower() == 'approved'):
approved = line[i+1:].strip()
break
if approved is not None:
if self.Authenticate([mm_cfg.AuthListAdmin,
mm_cfg.AuthListModerator],
approved) <> mm_cfg.UnAuthorized:
action = mm_cfg.APPROVE
else:
self.pend_repend(cookie, rec)
raise Errors.MMBadPasswordError
else:
action = mm_cfg.DISCARD
try:
self.HandleRequest(id, action)
except KeyError:
syslog('error', 'Could not process HELD_MESSAGE: %s', id)
return (op,)
elif op == Pending.RE_ENABLE:
member = data[1]
self.setDeliveryStatus(member, MemberAdaptor.ENABLED)
return op, member
else:
assert 0, 'Bad op: %s' % op
def ConfirmUnsubscription(self, addr, lang=None, remote=None):
if lang is None:
lang = self.getMemberLanguage(addr)
cookie = self.pend_new(Pending.UNSUBSCRIPTION, addr)
confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
cookie)
realname = self.real_name
if remote is not None:
by = " " + remote
remote = _(" from %(remote)s")
else:
by = ""
remote = ""
text = Utils.maketext(
'unsub.txt',
{'email' : addr,
'listaddr' : self.GetListEmail(),
'listname' : realname,
'cookie' : cookie,
'requestaddr' : self.getListAddress('request'),
'remote' : remote,
'listadmin' : self.GetOwnerEmail(),
'confirmurl' : confirmurl,
}, lang=lang, mlist=self)
msg = Message.UserNotification(
addr, self.GetRequestEmail(cookie),
text=text, lang=lang)
del msg['subject']
msg['Subject'] = self.GetConfirmLeaveSubject(realname, cookie)
msg['Reply-To'] = self.GetRequestEmail(cookie)
msg.send(self)
def HasExplicitDest(self, msg):
"""True if list name or any acceptable_alias is included among the
addresses in the recipient headers.
"""
listfullname = '%s@%s' % (self.internal_name(), self.host_name)
recips = []
to = []
for header in ('to', 'cc', 'resent-to', 'resent-cc'):
to.extend(getaddresses(msg.get_all(header, [])))
for fullname, addr in to:
if addr is None:
continue
addr = addr.lower()
localpart = addr.split('@')[0]
if ( localpart == self.internal_name() or
addr == listfullname):
return True
recips.append((addr, localpart))
def domatch(pattern, addr):
try:
if re.match(pattern, addr, re.IGNORECASE):
return True
except re.error:
if re.match(re.escape(pattern), addr, re.IGNORECASE):
return True
return False
aliases = self.acceptable_aliases.splitlines()
for addr, localpart in recips:
for alias in aliases:
stripped = alias.strip()
if not stripped:
continue
if '@' not in stripped and domatch(stripped, localpart):
return True
if domatch(stripped, addr):
return True
return False
def parse_matching_header_opt(self):
"""Return a list of triples [(field name, regex, line), ...]."""
all = []
for line in self.bounce_matching_headers.split('\n'):
line = line.strip()
if not line or line[0] == "#":
continue
i = line.find(':')
if i < 0:
syslog('config', 'bad bounce_matching_header line: %s\n%s',
self.real_name, line)
else:
header = line[:i]
value = line[i+1:].lstrip()
try:
cre = re.compile(value, re.IGNORECASE)
except re.error, e:
syslog('config', '''\
bad regexp in bounce_matching_header line: %s
\n%s (cause: %s)''', self.real_name, value, e)
else:
all.append((header, cre, line))
return all
def hasMatchingHeader(self, msg):
"""Return true if named header field matches a regexp in the
bounce_matching_header list variable.
Returns constraint line which matches or empty string for no
matches.
"""
for header, cre, line in self.parse_matching_header_opt():
for value in msg.get_all(header, []):
if cre.search(value):
return line
return 0
def autorespondToSender(self, sender, lang=None):
"""Return true if Mailman should auto-respond to this sender.
This is only consulted for messages sent to the -request address, or
for posting hold notifications, and serves only as a safety value for
mail loops with email 'bots.
"""
if lang == None:
lang = self.preferred_language
i18n.set_language(lang)
if mm_cfg.MAX_AUTORESPONSES_PER_DAY == 0:
return 1
today = time.localtime()[:3]
info = self.hold_and_cmd_autoresponses.get(sender)
if info is None or info[0] <> today:
self.hold_and_cmd_autoresponses[sender] = (today, 1)
return 1
date, count = info
if count < 0:
syslog('vette', '-request/hold autoresponse discarded for: %s',
sender)
return 0
if count >= mm_cfg.MAX_AUTORESPONSES_PER_DAY:
syslog('vette', '-request/hold autoresponse limit hit for: %s',
sender)
self.hold_and_cmd_autoresponses[sender] = (today, -1)
text = Utils.maketext(
'nomoretoday.txt',
{'sender' : sender,
'listname': '%s@%s' % (self.real_name, self.host_name),
'num' : count,
'owneremail': self.GetOwnerEmail(),
},
lang=lang)
msg = Message.UserNotification(
sender, self.GetOwnerEmail(),
_('Last autoresponse notification for today'),
text, lang=lang)
msg.send(self)
return 0
self.hold_and_cmd_autoresponses[sender] = (today, count+1)
return 1
def GetBannedPattern(self, email):
"""Returns matched entry in ban_list if email matches.
Otherwise returns None.
"""
ban = False
for pattern in self.ban_list:
if pattern.startswith('^'):
try:
if re.search(pattern, email, re.IGNORECASE):
ban = True
break
except re.error:
pass
else:
if pattern.lower() == email.lower():
ban = True
break
if ban:
return pattern
else:
return None
def GetAvailableLanguages(self):
langs = self.available_languages
if mm_cfg.DEFAULT_SERVER_LANGUAGE not in langs:
langs.append(mm_cfg.DEFAULT_SERVER_LANGUAGE)
return [lang for lang in langs if mm_cfg.LC_DESCRIPTIONS.has_key(lang)]