1 # Copyright (C) 2001-2007 by the Free Software Foundation, Inc.
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.
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.
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,
18 """Creation/deletion hooks for the Postfix MTA."""
20 from __future__
import with_statement
31 from Mailman
import Utils
32 from Mailman
.MTA
.Utils
import makealiases
33 from Mailman
.configuration
import config
34 from Mailman
.i18n
import _
35 from Mailman
.lockfile
import LockFile
37 LOCKFILE
= os
.path
.join(config
.LOCK_DIR
, 'creator')
38 ALIASFILE
= os
.path
.join(config
.DATA_DIR
, 'aliases')
39 VIRTFILE
= os
.path
.join(config
.DATA_DIR
, 'virtual-mailman')
40 TRPTFILE
= os
.path
.join(config
.DATA_DIR
, 'transport')
42 log
= logging
.getLogger('mailman.error')
47 msg
= 'command failed: %s (status: %s, %s)'
49 tcmd
= config
.POSTFIX_MAP_CMD
+ ' ' + TRPTFILE
50 status
= (os
.system(tcmd
) >> 8) & 0xff
52 errstr
= os
.strerror(status
)
53 log
.error(msg
, tcmd
, status
, errstr
)
54 raise RuntimeError(msg
% (tcmd
, status
, errstr
))
55 acmd
= config
.POSTFIX_ALIAS_CMD
+ ' ' + ALIASFILE
56 status
= (os
.system(acmd
) >> 8) & 0xff
58 errstr
= os
.strerror(status
)
59 log
.error(msg
, acmd
, status
, errstr
)
60 raise RuntimeError(msg
% (acmd
, status
, errstr
))
61 if os
.path
.exists(VIRTFILE
):
62 vcmd
= config
.POSTFIX_MAP_CMD
+ ' ' + VIRTFILE
63 status
= (os
.system(vcmd
) >> 8) & 0xff
65 errstr
= os
.strerror(status
)
66 log
.error(msg
, vcmd
, status
, errstr
)
67 raise RuntimeError(msg
% (vcmd
, status
, errstr
))
71 def _zapfile(filename
):
72 # Truncate the file w/o messing with the file permissions, but only if it
74 if os
.path
.exists(filename
):
75 fp
= open(filename
, 'w')
86 def _addlist(mlist
, fp
):
87 # Set up the mailman-loop address
88 loopaddr
= Utils
.ParseEmail(Utils
.get_site_noreply())[0]
89 loopmbox
= os
.path
.join(config
.DATA_DIR
, 'owner-bounces.mbox')
90 # Seek to the end of the text file, but if it's empty write the standard
91 # disclaimer, and the loop catch address.
95 # This file is generated by Mailman, and is kept in sync with the
96 # binary hash file aliases.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE
97 # unless you know what you're doing, and can keep the two files properly
98 # in sync. If you screw it up, you're on your own.
100 print >> fp
, '# The ultimate loop stopper address'
101 print >> fp
, '%s: %s' % (loopaddr
, loopmbox
)
103 # Bootstrapping. bin/genaliases must be run before any lists are created,
104 # but if no lists exist yet then mlist is None. The whole point of the
105 # exercise is to get the minimal aliases.db file into existance.
108 listname
= mlist
.internal_name()
109 hostname
= mlist
.host_name
110 fieldsz
= len(listname
) + len('-unsubscribe')
111 # The text file entries get a little extra info
112 print >> fp
, '# STANZA START: %s@%s' % (listname
, hostname
)
113 print >> fp
, '# CREATED:', time
.ctime(time
.time())
114 # Now add all the standard alias entries
115 for k
, v
in makealiases(mlist
):
117 if hostname
in config
.POSTFIX_STYLE_VIRTUAL_DOMAINS
:
118 k
+= config
.POSTFIX_VIRTUAL_SEPARATOR
+ hostname
119 # Format the text file nicely
120 print >> fp
, k
+ ':', ((fieldsz
- l
) * ' ') + v
121 # Finish the text file stanza
122 print >> fp
, '# STANZA END: %s@%s' % (listname
, hostname
)
127 def _addvirtual(mlist
, fp
):
128 listname
= mlist
.internal_name()
129 fieldsz
= len(listname
) + len('-unsubscribe')
130 hostname
= mlist
.host_name
131 # Set up the mailman-loop address
132 loopaddr
= mlist
.no_reply_address
133 loopdest
= Utils
.ParseEmail(loopaddr
)[0]
134 # Seek to the end of the text file, but if it's empty write the standard
135 # disclaimer, and the loop catch address.
139 # This file is generated by Mailman, and is kept in sync with the binary hash
140 # file virtual-mailman.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you
141 # know what you're doing, and can keep the two files properly in sync. If you
142 # screw it up, you're on your own.
144 # Note that you should already have this virtual domain set up properly in
145 # your Postfix installation. See README.POSTFIX for details.
147 # LOOP ADDRESSES START
150 """ % (loopaddr
, loopdest
)
151 # The text file entries get a little extra info
152 print >> fp
, '# STANZA START: %s@%s' % (listname
, hostname
)
153 print >> fp
, '# CREATED:', time
.ctime(time
.time())
154 # Now add all the standard alias entries
155 for k
, v
in makealiases(mlist
):
156 fqdnaddr
= '%s@%s' % (k
, hostname
)
158 # Format the text file nicely
159 if hostname
in config
.POSTFIX_STYLE_VIRTUAL_DOMAINS
:
160 k
+= config
.POSTFIX_VIRTUAL_SEPARATOR
+ hostname
161 print >> fp
, fqdnaddr
, ((fieldsz
- l
) * ' '), k
162 # Finish the text file stanza
163 print >> fp
, '# STANZA END: %s@%s' % (listname
, hostname
)
169 def _check_for_virtual_loopaddr(mlist
, filename
, func
):
170 loopaddr
= mlist
.no_reply_address
171 loopdest
= Utils
.ParseEmail(loopaddr
)[0]
172 if func
is _addtransport
:
173 loopdest
= 'local:' + loopdest
174 infp
= open(filename
)
175 outfp
= open(filename
+ '.tmp', 'w')
177 # Find the start of the loop address block
179 line
= infp
.readline()
183 if line
.startswith('# LOOP ADDRESSES START'):
185 # Now see if our domain has already been written
187 line
= infp
.readline()
190 if line
.startswith('# LOOP ADDRESSES END'):
192 print >> outfp
, '%s\t%s' % (loopaddr
, loopdest
)
195 elif line
.startswith(loopaddr
):
200 # This isn't our loop address, so spit it out and continue
202 outfp
.writelines(infp
.readlines())
206 os
.rename(filename
+ '.tmp', filename
)
210 def _addtransport(mlist
, fp
):
211 # Set up the mailman-loop address
212 loopaddr
= mlist
.no_reply_address
213 loopdest
= Utils
.ParseEmail(loopaddr
)[0]
214 # create/add postfix transport file for mailman
218 # This file is generated by Mailman, and is kept in sync with the
219 # binary hash file transport.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE
220 # unless you know what you're doing, and can keep the two files properly
221 # in sync. If you screw it up, you're on your own.
223 # LOOP ADDRESSES START
226 """ % (loopaddr
, loopdest
)
227 # List LMTP_ONLY_DOMAINS
228 if config
.LMTP_ONLY_DOMAINS
:
229 print >> fp
, '# LMTP ONLY DOMAINS START'
230 for dom
in config
.LMTP_ONLY_DOMAINS
:
231 print >> fp
, '%s\tlmtp:%s:%s' % (dom
,
234 print >> fp
, '# LMTP ONLY DOMAINS END\n'
235 listname
= mlist
.internal_name()
236 hostname
= mlist
.host_name
237 # No need of individual local part if the domain is LMTP only
238 if hostname
in config
.LMTP_ONLY_DOMAINS
:
240 fieldsz
= len(listname
) + len(hostname
) + len('-unsubscribe') + 1
241 # The text file entries get a little extra info
242 print >> fp
, '# STANZA START: %s@%s' % (listname
, hostname
)
243 print >> fp
, '# CREATED:', time
.ctime(time
.time())
244 # Now add transport entries
245 for k
, v
in makealiases(mlist
):
246 l
= len(k
+ hostname
) + 1
247 print >> fp
, '%s@%s' % (k
, hostname
), ((fieldsz
- l
) * ' ')\
248 + 'lmtp:%s:%s' % (config
.LMTP_HOST
, config
.LMTP_PORT
)
250 print >> fp
, '# STANZA END: %s@%s' % (listname
, hostname
)
255 def _do_create(mlist
, textfile
, func
):
256 # Crack open the plain text file
258 fp
= open(textfile
, 'r+')
260 if e
.errno
<> errno
.ENOENT
: raise
261 fp
= open(textfile
, 'w+')
266 # Now double check the virtual plain text file
267 if func
in (_addvirtual
, _addtransport
):
268 _check_for_virtual_loopaddr(mlist
, textfile
, func
)
271 def create(mlist
, cgi
=False, nolock
=False, quiet
=False):
272 # Acquire the global list database lock. quiet flag is ignored.
278 # Do the aliases file, which always needs to be done
281 _do_create(mlist
, TRPTFILE
, _addtransport
)
282 _do_create(None, ALIASFILE
, _addlist
)
284 _do_create(mlist
, ALIASFILE
, _addlist
)
285 if mlist
.host_name
in config
.POSTFIX_STYLE_VIRTUAL_DOMAINS
:
286 _do_create(mlist
, VIRTFILE
, _addvirtual
)
290 lock
.unlock(unconditionally
=True)
294 def _do_remove(mlist
, textfile
):
295 listname
= mlist
.internal_name()
296 hostname
= mlist
.host_name
297 # Now do our best to filter out the proper stanza from the text file.
298 # The text file better exist!
301 infp
= open(textfile
)
303 if e
.errno
<> errno
.ENOENT
: raise
304 # Otherwise, there's no text file to filter so we're done.
307 outfp
= open(textfile
+ '.tmp', 'w')
309 start
= '# STANZA START: %s@%s' % (listname
, hostname
)
310 end
= '# STANZA END: %s@%s' % (listname
, hostname
)
312 line
= infp
.readline()
315 # If we're filtering out a stanza, just look for the end marker and
316 # filter out everything in between. If we're not in the middle of
317 # filtering out a stanza, we're just looking for the proper begin
320 if line
.strip() == end
:
322 # Discard the trailing blank line, but don't worry if
323 # we're at the end of the file.
325 # Otherwise, ignore the line
327 if line
.strip() == start
:
328 # Filter out this stanza
332 # Close up shop, and rotate the files
336 os
.rename(textfile
+'.tmp', textfile
)
339 def remove(mlist
, cgi
=False):
340 # Acquire the global list database lock
341 with
LockFile(LOCKFILE
):
343 _do_remove(mlist
, TRPTFILE
)
345 _do_remove(mlist
, ALIASFILE
)
346 if mlist
.host_name
in config
.POSTFIX_STYLE_VIRTUAL_DOMAINS
:
347 _do_remove(mlist
, VIRTFILE
)
348 # Regenerate the alias and map files
354 def checkperms(state
):
355 targetmode
= S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
356 for file in ALIASFILE
, VIRTFILE
, TRPTFILE
:
358 print _('checking permissions on %(file)s')
363 if e
.errno
<> errno
.ENOENT
:
365 if stat
and (stat
[ST_MODE
] & targetmode
) <> targetmode
:
367 octmode
= oct(stat
[ST_MODE
])
368 print _('%(file)s permissions must be 066x (got %(octmode)s)'),
371 os
.chmod(file, stat
[ST_MODE
] | targetmode
)
374 # Make sure the corresponding .db files are owned by the Mailman user.
375 # We don't need to check the group ownership of the file, since
376 # check_perms checks this itself.
377 dbfile
= file + '.db'
380 stat
= os
.stat(dbfile
)
382 if e
.errno
<> errno
.ENOENT
:
386 print _('checking ownership of %(dbfile)s')
387 user
= config
.MAILMAN_USER
388 ownerok
= stat
[ST_UID
] == pwd
.getpwnam(user
)[2]
391 owner
= pwd
.getpwuid(stat
[ST_UID
])[0]
393 owner
= 'uid %d' % stat
[ST_UID
]
394 print _('%(dbfile)s owned by %(owner)s (must be owned by %(user)s'),
398 uid
= pwd
.getpwnam(user
)[2]
399 gid
= grp
.getgrnam(config
.MAILMAN_GROUP
)[2]
400 os
.chown(dbfile
, uid
, gid
)
403 if stat
and (stat
[ST_MODE
] & targetmode
) <> targetmode
:
405 octmode
= oct(stat
[ST_MODE
])
406 print _('%(dbfile)s permissions must be 066x (got %(octmode)s)'),
409 os
.chmod(dbfile
, stat
[ST_MODE
] | targetmode
)