1203248f54801365f22afdf455646c239aac5f4f
[mspang/vmailman.git] / Mailman / MTA / Postfix.py
1 # Copyright (C) 2001-2005 by the Free Software Foundation, Inc.
2 #
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16 # USA.
17
18 """Creation/deletion hooks for the Postfix MTA."""
19
20 import os
21 import pwd
22 import grp
23 import time
24 import errno
25 from stat import *
26
27 from Mailman import mm_cfg
28 from Mailman import Utils
29 from Mailman import LockFile
30 from Mailman.i18n import _
31 from Mailman.MTA.Utils import makealiases
32 from Mailman.Logging.Syslog import syslog
33
34 LOCKFILE = os.path.join(mm_cfg.LOCK_DIR, 'creator')
35 ALIASFILE = os.path.join(mm_cfg.DATA_DIR, 'aliases')
36 VIRTFILE = os.path.join(mm_cfg.DATA_DIR, 'virtual-mailman')
37
38 try:
39     True, False
40 except NameError:
41     True = 1
42     False = 0
43
44
45 \f
46 def _update_maps():
47     msg = 'command failed: %s (status: %s, %s)'
48     acmd = mm_cfg.POSTFIX_ALIAS_CMD + ' ' + ALIASFILE
49     status = (os.system(acmd) >> 8) & 0xff
50     if status:
51         errstr = os.strerror(status)
52         syslog('error', msg, acmd, status, errstr)
53         raise RuntimeError, msg % (acmd, status, errstr)
54     if os.path.exists(VIRTFILE):
55         vcmd = mm_cfg.POSTFIX_MAP_CMD + ' ' + VIRTFILE
56         status = (os.system(vcmd) >> 8) & 0xff
57         if status:
58             errstr = os.strerror(status)
59             syslog('error', msg, vcmd, status, errstr)
60             raise RuntimeError, msg % (vcmd, status, errstr)
61
62
63 \f
64 def makelock():
65     return LockFile.LockFile(LOCKFILE)
66
67
68 def _zapfile(filename):
69     # Truncate the file w/o messing with the file permissions, but only if it
70     # already exists.
71     if os.path.exists(filename):
72         fp = open(filename, 'w')
73         fp.close()
74
75
76 def clear():
77     _zapfile(ALIASFILE)
78     _zapfile(VIRTFILE)
79
80
81 \f
82 def _addlist(mlist, fp):
83     # Set up the mailman-loop address
84     loopaddr = Utils.ParseEmail(Utils.get_site_email(extra='loop'))[0]
85     loopmbox = os.path.join(mm_cfg.DATA_DIR, 'owner-bounces.mbox')
86     # Seek to the end of the text file, but if it's empty write the standard
87     # disclaimer, and the loop catch address.
88     fp.seek(0, 2)
89     if not fp.tell():
90         print >> fp, """\
91 # This file is generated by Mailman, and is kept in sync with the
92 # binary hash file aliases.db.  YOU SHOULD NOT MANUALLY EDIT THIS FILE
93 # unless you know what you're doing, and can keep the two files properly
94 # in sync.  If you screw it up, you're on your own.
95 """
96         print >> fp, '# The ultimate loop stopper address'
97         print >> fp, '%s: %s' % (loopaddr, loopmbox)
98         print >> fp
99     # Bootstrapping.  bin/genaliases must be run before any lists are created,
100     # but if no lists exist yet then mlist is None.  The whole point of the
101     # exercise is to get the minimal aliases.db file into existance.
102     if mlist is None:
103         return
104     listname = mlist.internal_name()
105     fieldsz = len(listname) + len('-unsubscribe')
106     # The text file entries get a little extra info
107     print >> fp, '# STANZA START:', listname
108     print >> fp, '# CREATED:', time.ctime(time.time())
109     # Now add all the standard alias entries
110     for k, v in makealiases(listname):
111         if mlist.host_name in mm_cfg.POSTFIX_STYLE_VIRTUAL_DOMAINS and mm_cfg.POSTFIX_MANGLE:
112             k = "%s-%s" % (k, mlist.host_name.replace('.','-'))
113         # Format the text file nicely
114         print >> fp, k + ':', ((fieldsz - len(k)) * ' ') + v
115     # Finish the text file stanza
116     print >> fp, '# STANZA END:', listname
117     print >> fp
118
119
120 \f
121 def _addvirtual(mlist, fp):
122     listname = mlist.internal_name()
123     fieldsz = len(listname) + len('-unsubscribe')
124     hostname = mlist.host_name
125     # Set up the mailman-loop address
126     loopaddr = Utils.get_site_email(mlist.host_name, extra='loop')
127     loopdest = Utils.ParseEmail(loopaddr)[0]
128     # Seek to the end of the text file, but if it's empty write the standard
129     # disclaimer, and the loop catch address.
130     fp.seek(0, 2)
131     if not fp.tell():
132         print >> fp, """\
133 # This file is generated by Mailman, and is kept in sync with the binary hash
134 # file virtual-mailman.db.  YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you
135 # know what you're doing, and can keep the two files properly in sync.  If you
136 # screw it up, you're on your own.
137 #
138 # Note that you should already have this virtual domain set up properly in
139 # your Postfix installation.  See README.POSTFIX for details.
140
141 # LOOP ADDRESSES START
142 %s\t%s
143 # LOOP ADDRESSES END
144 """ % (loopaddr, loopdest)
145     # The text file entries get a little extra info
146     print >> fp, '# STANZA START:', listname
147     print >> fp, '# CREATED:', time.ctime(time.time())
148     # Now add all the standard alias entries
149     for k, v in makealiases(listname):
150         fqdnaddr = '%s@%s' % (k, hostname)
151         if mm_cfg.POSTFIX_MANGLE:
152             k = "%s-%s" % (k, hostname.replace('.','-'))
153         # Format the text file nicely
154         print >> fp, fqdnaddr, ((fieldsz - len(k)) * ' '), k
155     # Finish the text file stanza
156     print >> fp, '# STANZA END:', listname
157     print >> fp
158
159
160 \f
161 # Blech.
162 def _check_for_virtual_loopaddr(mlist, filename):
163     loopaddr = Utils.get_site_email(mlist.host_name, extra='loop')
164     loopdest = Utils.ParseEmail(loopaddr)[0]
165     infp = open(filename)
166     omask = os.umask(007)
167     try:
168         outfp = open(filename + '.tmp', 'w')
169     finally:
170         os.umask(omask)
171     try:
172         # Find the start of the loop address block
173         while True:
174             line = infp.readline()
175             if not line:
176                 break
177             outfp.write(line)
178             if line.startswith('# LOOP ADDRESSES START'):
179                 break
180         # Now see if our domain has already been written
181         while True:
182             line = infp.readline()
183             if not line:
184                 break
185             if line.startswith('# LOOP ADDRESSES END'):
186                 # It hasn't
187                 print >> outfp, '%s\t%s' % (loopaddr, loopdest)
188                 outfp.write(line)
189                 break
190             elif line.startswith(loopaddr):
191                 # We just found it
192                 outfp.write(line)
193                 break
194             else:
195                 # This isn't our loop address, so spit it out and continue
196                 outfp.write(line)
197         outfp.writelines(infp.readlines())
198     finally:
199         infp.close()
200         outfp.close()
201     os.rename(filename + '.tmp', filename)
202
203
204 \f
205 def _do_create(mlist, textfile, func):
206     # Crack open the plain text file
207     try:
208         fp = open(textfile, 'r+')
209     except IOError, e:
210         if e.errno <> errno.ENOENT: raise
211         omask = os.umask(007)
212         try:
213             fp = open(textfile, 'w+')
214         finally:
215             os.umask(omask)
216     try:
217         func(mlist, fp)
218     finally:
219         fp.close()
220     # Now double check the virtual plain text file
221     if func is _addvirtual:
222         _check_for_virtual_loopaddr(mlist, textfile)
223
224
225 def create(mlist, cgi=False, nolock=False, quiet=False):
226     # Acquire the global list database lock.  quiet flag is ignored.
227     lock = None
228     if not nolock:
229         lock = makelock()
230         lock.lock()
231     # Do the aliases file, which need to be done in any case
232     try:
233         _do_create(mlist, ALIASFILE, _addlist)
234         if mlist and mlist.host_name in mm_cfg.POSTFIX_STYLE_VIRTUAL_DOMAINS:
235             _do_create(mlist, VIRTFILE, _addvirtual)
236         _update_maps()
237     finally:
238         if lock:
239             lock.unlock(unconditionally=True)
240
241
242 \f
243 def _do_remove(mlist, textfile, virtualp):
244     listname = mlist.internal_name()
245     # Now do our best to filter out the proper stanza from the text file.
246     # The text file better exist!
247     outfp = None
248     try:
249         infp = open(textfile)
250     except IOError, e:
251         if e.errno <> errno.ENOENT: raise
252         # Otherwise, there's no text file to filter so we're done.
253         return
254     try:
255         omask = os.umask(007)
256         try:
257             outfp = open(textfile + '.tmp', 'w')
258         finally:
259             os.umask(omask)
260         filteroutp = False
261         start = '# STANZA START: ' + listname
262         end = '# STANZA END: ' + listname
263         while 1:
264             line = infp.readline()
265             if not line:
266                 break
267             # If we're filtering out a stanza, just look for the end marker and
268             # filter out everything in between.  If we're not in the middle of
269             # filtering out a stanza, we're just looking for the proper begin
270             # marker.
271             if filteroutp:
272                 if line.strip() == end:
273                     filteroutp = False
274                     # Discard the trailing blank line, but don't worry if
275                     # we're at the end of the file.
276                     infp.readline()
277                 # Otherwise, ignore the line
278             else:
279                 if line.strip() == start:
280                     # Filter out this stanza
281                     filteroutp = True
282                 else:
283                     outfp.write(line)
284     # Close up shop, and rotate the files
285     finally:
286         infp.close()
287         outfp.close()
288     os.rename(textfile+'.tmp', textfile)
289
290
291 def remove(mlist, cgi=False):
292     # Acquire the global list database lock
293     lock = makelock()
294     lock.lock()
295     try:
296         _do_remove(mlist, ALIASFILE, False)
297         if mlist.host_name in mm_cfg.POSTFIX_STYLE_VIRTUAL_DOMAINS:
298             _do_remove(mlist, VIRTFILE, True)
299         # Regenerate the alias and map files
300         _update_maps()
301     finally:
302         lock.unlock(unconditionally=True)
303
304
305 \f
306 def checkperms(state):
307     targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
308     for file in ALIASFILE, VIRTFILE:
309         if state.VERBOSE:
310             print _('checking permissions on %(file)s')
311         stat = None
312         try:
313             stat = os.stat(file)
314         except OSError, e:
315             if e.errno <> errno.ENOENT:
316                 raise
317         if stat and (stat[ST_MODE] & targetmode) <> targetmode:
318             state.ERRORS += 1
319             octmode = oct(stat[ST_MODE])
320             print _('%(file)s permissions must be 066x (got %(octmode)s)'),
321             if state.FIX:
322                 print _('(fixing)')
323                 os.chmod(file, stat[ST_MODE] | targetmode)
324             else:
325                 print
326         # Make sure the corresponding .db files are owned by the Mailman user.
327         # We don't need to check the group ownership of the file, since
328         # check_perms checks this itself.
329         dbfile = file + '.db'
330         stat = None
331         try:
332             stat = os.stat(dbfile)
333         except OSError, e:
334             if e.errno <> errno.ENOENT:
335                 raise
336             continue
337         if state.VERBOSE:
338             print _('checking ownership of %(dbfile)s')
339         user = mm_cfg.MAILMAN_USER
340         ownerok = stat[ST_UID] == pwd.getpwnam(user)[2]
341         if not ownerok:
342             try:
343                 owner = pwd.getpwuid(stat[ST_UID])[0]
344             except KeyError:
345                 owner = 'uid %d' % stat[ST_UID]
346             print _('%(dbfile)s owned by %(owner)s (must be owned by %(user)s'),
347             state.ERRORS += 1
348             if state.FIX:
349                 print _('(fixing)')
350                 uid = pwd.getpwnam(user)[2]
351                 gid = grp.getgrnam(mm_cfg.MAILMAN_GROUP)[2]
352                 os.chown(dbfile, uid, gid)
353             else:
354                 print
355         if stat and (stat[ST_MODE] & targetmode) <> targetmode:
356             state.ERRORS += 1
357             octmode = oct(stat[ST_MODE])
358             print _('%(dbfile)s permissions must be 066x (got %(octmode)s)'),
359             if state.FIX:
360                 print _('(fixing)')
361                 os.chmod(dbfile, stat[ST_MODE] | targetmode)
362             else:
363                 print