"""Mixin class for MailList which handles administrative requests.
Two types of admin requests are currently supported: adding members to a
closed or semi-closed list, and moderated posts.
Pending subscriptions which are requiring a user's confirmation are handled
elsewhere.
"""
import os
import time
import marshal
import errno
import cPickle
from cStringIO import StringIO
import email
from email.MIMEMessage import MIMEMessage
from email.Generator import Generator
from email.Utils import getaddresses
from Mailman import mm_cfg
from Mailman import Utils
from Mailman import Message
from Mailman import Errors
from Mailman.UserDesc import UserDesc
from Mailman.Queue.sbcache import get_switchboard
from Mailman.Logging.Syslog import syslog
from Mailman import i18n
_ = i18n._
IGN = 0
HELDMSG = 1
SUBSCRIPTION = 2
UNSUBSCRIPTION = 3
DEFER = 0
REMOVE = 1
LOST = 2
DASH = '-'
NL = '\n'
class ListAdmin:
def InitVars(self):
self.next_request_id = 1
def InitTempVars(self):
self.__db = None
def __filename(self):
return os.path.join(self.fullpath(), 'request.db')
def __opendb(self):
filename = self.__filename()
if self.__db is None:
assert self.Locked()
try:
fp = open(filename)
self.__db = marshal.load(fp)
fp.close()
except IOError, e:
if e.errno <> errno.ENOENT: raise
self.__db = {}
except EOFError, e:
syslog('error',
'request.db file corrupt for list %s, blowing it away.',
self.internal_name())
self.__db = {}
type, version = self.__db.get('version', (IGN, None))
if version is None:
for id, (type, data) in self.__db.items():
if type == IGN:
pass
elif type == HELDMSG and len(data) == 5:
self.__db[id] = data + ({},)
elif type == SUBSCRIPTION:
if len(data) == 4:
stime, addr, password, digest = data
lang = self.preferred_language
data = stime, addr, '', password, digest, lang
elif len(data) == 5:
stime, addr, password, digest, lang = data
data = stime, addr, '', password, digest, lang
self.__db[id] = type, data
def __closedb(self):
if self.__db is not None:
assert self.Locked()
self.__db['version'] = IGN, mm_cfg.REQUESTS_FILE_SCHEMA_VERSION
tmpfile = self.__filename() + '.tmp'
omask = os.umask(002)
try:
fp = open(tmpfile, 'w')
marshal.dump(self.__db, fp)
fp.close()
self.__db = None
finally:
os.umask(omask)
os.rename(tmpfile, self.__filename())
def __request_id(self):
id = self.next_request_id
self.next_request_id += 1
return id
def SaveRequestsDb(self):
self.__closedb()
def NumRequestsPending(self):
self.__opendb()
if self.__db.has_key('version'):
return len(self.__db) - 1
return len(self.__db)
def __getmsgids(self, rtype):
self.__opendb()
ids = [k for k, (type, data) in self.__db.items() if type == rtype]
ids.sort()
return ids
def GetHeldMessageIds(self):
return self.__getmsgids(HELDMSG)
def GetSubscriptionIds(self):
return self.__getmsgids(SUBSCRIPTION)
def GetUnsubscriptionIds(self):
return self.__getmsgids(UNSUBSCRIPTION)
def GetRecord(self, id):
self.__opendb()
type, data = self.__db[id]
return data
def GetRecordType(self, id):
self.__opendb()
type, data = self.__db[id]
return type
def HandleRequest(self, id, value, comment=None, preserve=None,
forward=None, addr=None):
self.__opendb()
rtype, data = self.__db[id]
if rtype == HELDMSG:
status = self.__handlepost(data, value, comment, preserve,
forward, addr)
elif rtype == UNSUBSCRIPTION:
status = self.__handleunsubscription(data, value, comment)
else:
assert rtype == SUBSCRIPTION
status = self.__handlesubscription(data, value, comment)
if status <> DEFER:
del self.__db[id]
def HoldMessage(self, msg, reason, msgdata={}):
newmsgdata = {}
newmsgdata.update(msgdata)
msgdata = newmsgdata
self.__opendb()
id = self.__request_id()
while self.__db.has_key(id):
id = self.__request_id()
sender = msg.get_sender()
if mm_cfg.HOLD_MESSAGES_AS_PICKLES:
ext = 'pck'
else:
ext = 'txt'
filename = 'heldmsg-%s-%d.%s' % (self.internal_name(), id, ext)
omask = os.umask(002)
fp = None
try:
fp = open(os.path.join(mm_cfg.DATA_DIR, filename), 'w')
if mm_cfg.HOLD_MESSAGES_AS_PICKLES:
cPickle.dump(msg, fp, 1)
else:
g = Generator(fp)
g(msg, 1)
finally:
if fp:
fp.close()
os.umask(omask)
msgsubject = msg.get('subject', _('(no subject)'))
data = time.time(), sender, msgsubject, reason, filename, msgdata
self.__db[id] = (HELDMSG, data)
return id
def __handlepost(self, record, value, comment, preserve, forward, addr):
ptime, sender, subject, reason, filename, msgdata = record
path = os.path.join(mm_cfg.DATA_DIR, filename)
if preserve:
parts = os.path.split(path)[1].split(DASH)
parts[0] = 'spam'
spamfile = DASH.join(parts)
try:
fp = open(path)
except IOError, e:
if e.errno <> errno.ENOENT: raise
return LOST
try:
msg = cPickle.load(fp)
finally:
fp.close()
outpath = os.path.join(mm_cfg.SPAM_DIR, spamfile)
head, ext = os.path.splitext(outpath)
outpath = head + '.msg'
outfp = open(outpath, 'w')
try:
g = Generator(outfp)
g(msg, 1)
finally:
outfp.close()
rejection = None
fp = None
msg = None
status = REMOVE
if value == mm_cfg.DEFER:
status = DEFER
elif value == mm_cfg.APPROVE:
try:
msg = readMessage(path)
except IOError, e:
if e.errno <> errno.ENOENT: raise
return LOST
msg = readMessage(path)
msgdata['approved'] = 1
msgdata['adminapproved'] = 1
try:
del msgdata['filebase']
except KeyError:
pass
msg['X-Mailman-Approved-At'] = email.Utils.formatdate(localtime=1)
syslog('vette', 'held message approved, message-id: %s',
msg.get('message-id', 'n/a'))
inq = get_switchboard(mm_cfg.INQUEUE_DIR)
inq.enqueue(msg, _metadata=msgdata)
elif value == mm_cfg.REJECT:
rejection = 'Refused'
self.__refuse(_('Posting of your message titled "%(subject)s"'),
sender, comment or _('[No reason given]'),
lang=self.getMemberLanguage(sender))
else:
assert value == mm_cfg.DISCARD
rejection = 'Discarded'
if forward and addr:
try:
copy = readMessage(path)
except IOError, e:
if e.errno <> errno.ENOENT: raise
raise Errors.LostHeldMessage(path)
addrs = getaddresses([addr])
if len(addrs) == 1:
realname, addr = addrs[0]
lang = self.getMemberLanguage(addr)
else:
addr = [a for realname, a in addrs]
lang = self.preferred_language
otrans = i18n.get_translation()
i18n.set_language(lang)
try:
fmsg = Message.UserNotification(
addr, self.GetBouncesEmail(),
_('Forward of moderated message'),
lang=lang)
finally:
i18n.set_translation(otrans)
fmsg.set_type('message/rfc822')
fmsg.attach(copy)
fmsg.send(self)
if rejection:
note = '''%(listname)s: %(rejection)s posting:
\tFrom: %(sender)s
\tSubject: %(subject)s''' % {
'listname' : self.internal_name(),
'rejection': rejection,
'sender' : sender.replace('%', '%%'),
'subject' : subject.replace('%', '%%'),
}
if comment:
note += '\n\tReason: ' + comment.replace('%', '%%')
syslog('vette', note)
if status <> DEFER:
try:
os.unlink(path)
except OSError, e:
if e.errno <> errno.ENOENT: raise
return LOST
return status
def HoldSubscription(self, addr, fullname, password, digest, lang):
self.__opendb()
id = self.__request_id()
assert not self.__db.has_key(id)
data = time.time(), addr, fullname, password, digest, lang
self.__db[id] = (SUBSCRIPTION, data)
syslog('vette', '%s: held subscription request from %s',
self.internal_name(), addr)
if self.admin_immed_notify:
realname = self.real_name
subject = _(
'New subscription request to list %(realname)s from %(addr)s')
text = Utils.maketext(
'subauth.txt',
{'username' : addr,
'listname' : self.internal_name(),
'hostname' : self.host_name,
'admindb_url': self.GetScriptURL('admindb', absolute=1),
}, mlist=self)
owneraddr = self.GetOwnerEmail()
msg = Message.UserNotification(owneraddr, owneraddr, subject, text,
self.preferred_language)
msg.send(self, **{'tomoderators': 1})
def __handlesubscription(self, record, value, comment):
stime, addr, fullname, password, digest, lang = record
if value == mm_cfg.DEFER:
return DEFER
elif value == mm_cfg.DISCARD:
pass
elif value == mm_cfg.REJECT:
self.__refuse(_('Subscription request'), addr,
comment or _('[No reason given]'),
lang=lang)
else:
assert value == mm_cfg.SUBSCRIBE
try:
userdesc = UserDesc(addr, fullname, password, digest, lang)
self.ApprovedAddMember(userdesc, whence='via admin approval')
except Errors.MMAlreadyAMember:
pass
self.__opendb()
return REMOVE
def HoldUnsubscription(self, addr):
self.__opendb()
id = self.__request_id()
assert not self.__db.has_key(id)
self.__db[id] = (UNSUBSCRIPTION, addr)
syslog('vette', '%s: held unsubscription request from %s',
self.internal_name(), addr)
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)
owneraddr = self.GetOwnerEmail()
msg = Message.UserNotification(owneraddr, owneraddr, subject, text,
self.preferred_language)
msg.send(self, **{'tomoderators': 1})
def __handleunsubscription(self, record, value, comment):
addr = record
if value == mm_cfg.DEFER:
return DEFER
elif value == mm_cfg.DISCARD:
pass
elif value == mm_cfg.REJECT:
self.__refuse(_('Unsubscription request'), addr, comment)
else:
assert value == mm_cfg.UNSUBSCRIBE
try:
self.ApprovedDeleteMember(addr)
except Errors.NotAMemberError:
pass
return REMOVE
def __refuse(self, request, recip, comment, origmsg=None, lang=None):
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:
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.GetBouncesEmail(),
subject, text, lang)
msg.send(self)
def _UpdateRecords(self):
self.__opendb()
for id, (type, info) in self.__db.items():
if type == SUBSCRIPTION:
if len(info) == 4:
when, addr, passwd, digest = info
fullname = ''
lang = self.preferred_language
elif len(info) == 5:
when, addr, passwd, digest, lang = info
fullname = ''
else:
assert len(info) == 6, 'Unknown subscription record layout'
continue
self.__db[id] = when, addr, fullname, passwd, digest, lang
elif type == HELDMSG:
if len(info) == 5:
when, sender, subject, reason, text = info
msgdata = {}
else:
assert len(info) == 6, 'Unknown held msg record layout'
continue
self.__db[id] = when, sender, subject, reason, text, msgdata
self.__closedb()
def readMessage(path):
ext = os.path.splitext(path)[1]
fp = open(path)
try:
if ext == '.txt':
msg = email.message_from_file(fp, Message.Message)
else:
assert ext == '.pck'
msg = cPickle.load(fp)
finally:
fp.close()
return msg