Apply 67_update_handle_old_versions.patch
[mspang/vmailman.git] / bin / update
1 #! @PYTHON@
2 #
3 # Copyright (C) 1998-2005 by the Free Software Foundation, Inc.
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
19 """Perform all necessary upgrades.
20
21 Usage: %(PROGRAM)s [options]
22
23 Options:
24     -f/--force
25         Force running the upgrade procedures.  Normally, if the version number
26         of the installed Mailman matches the current version number (or a
27         `downgrade' is detected), nothing will be done.
28
29     -h/--help
30         Print this text and exit.
31
32 Use this script to help you update to the latest release of Mailman from
33 some previous version.  It knows about versions back to 1.0b4 (?).
34 """
35
36 import os
37 import md5
38 import sys
39 import time
40 import errno
41 import getopt
42 import shutil
43 import cPickle
44 import marshal
45
46 import paths
47 import email
48
49 from Mailman import mm_cfg
50 from Mailman import Utils
51 from Mailman import MailList
52 from Mailman import Message
53 from Mailman import Pending
54 from Mailman.LockFile import TimeOutError
55 from Mailman.i18n import _
56 from Mailman.Queue.Switchboard import Switchboard
57 from Mailman.OldStyleMemberships import OldStyleMemberships
58 from Mailman.MemberAdaptor import BYBOUNCE, ENABLED
59
60 FRESH = 0
61 NOTFRESH = -1
62
63 LMVFILE = os.path.join(mm_cfg.DATA_DIR, 'last_mailman_version')
64 PROGRAM = sys.argv[0]
65
66
67 \f
68 def calcversions():
69     # Returns a tuple of (lastversion, thisversion).  If the last version
70     # could not be determined, lastversion will be FRESH or NOTFRESH,
71     # depending on whether this installation appears to be fresh or not.  The
72     # determining factor is whether there are files in the $var_prefix/logs
73     # subdir or not.  The version numbers are HEX_VERSIONs.
74     #
75     # See if we stored the last updated version
76     lastversion = None
77     thisversion = mm_cfg.HEX_VERSION
78     try:
79         fp = open(LMVFILE)
80         data = fp.read()
81         fp.close()
82         lastversion = int(data, 16)
83     except (IOError, ValueError):
84         pass
85     #
86     # try to figure out if this is a fresh install
87     if lastversion is None:
88         lastversion = FRESH
89         try:
90             if os.listdir(mm_cfg.LOG_DIR):
91                 lastversion = NOTFRESH
92         except OSError:
93             pass
94     return (lastversion, thisversion)
95
96
97 \f
98 def makeabs(relpath):
99     return os.path.join(mm_cfg.PREFIX, relpath)
100
101 def make_varabs(relpath):
102     return os.path.join(mm_cfg.VAR_PREFIX, relpath)
103
104 \f
105 def move_language_templates(mlist):
106     listname = mlist.internal_name()
107     print _('Fixing language templates: %(listname)s')
108     # Mailman 2.1 has a new cascading search for its templates, defined and
109     # described in Utils.py:maketext().  Putting templates in the top level
110     # templates/ subdir or the lists/<listname> subdir is deprecated and no
111     # longer searched..
112     #
113     # What this means is that most templates can live in the global templates/
114     # subdirectory, and only needs to be copied into the list-, vhost-, or
115     # site-specific language directories when needed.
116     #
117     # Also, by default all standard (i.e. English) templates must now live in
118     # the templates/en directory.  This update cleans up all the templates,
119     # deleting more-specific duplicates (as calculated by md5 checksums) in
120     # favor of more-global locations.
121     #
122     # First, get rid of any lists/<list> template or lists/<list>/en template
123     # that is identical to the global templates/* default.
124     for gtemplate in os.listdir(os.path.join(mm_cfg.TEMPLATE_DIR, mm_cfg.DEFAULT_SERVER_LANGUAGE)):
125         # BAW: get rid of old templates, e.g. admlogin.txt and
126         # handle_opts.html
127         try:
128             fp = open(os.path.join(mm_cfg.TEMPLATE_DIR, gtemplate))
129         except IOError, e:
130             if not (e.errno in [errno.ENOENT, errno.EISDIR, ]): raise
131             # No global template
132             continue
133
134         gcksum = md5.new(fp.read()).digest()
135         fp.close()
136         # Match against the lists/<list>/* template
137         try:
138             fp = open(os.path.join(mlist.fullpath(), gtemplate))
139         except IOError, e:
140             if e.errno <> errno.ENOENT: raise
141         else:
142             tcksum = md5.new(fp.read()).digest()
143             fp.close()
144             if gcksum == tcksum:
145                 os.unlink(os.path.join(mlist.fullpath(), gtemplate))
146         # Match against the lists/<list>/*.prev template
147         try:
148             fp = open(os.path.join(mlist.fullpath(), gtemplate + '.prev'))
149         except IOError, e:
150             if e.errno <> errno.ENOENT: raise
151         else:
152             tcksum = md5.new(fp.read()).digest()
153             fp.close()
154             if gcksum == tcksum:
155                 os.unlink(os.path.join(mlist.fullpath(), gtemplate + '.prev'))
156         # Match against the lists/<list>/en/* templates
157         try:
158             fp = open(os.path.join(mlist.fullpath(), mm_cfg.DEFAULT_SERVER_LANGUAGE, gtemplate))
159         except IOError, e:
160             if e.errno <> errno.ENOENT: raise
161         else:
162             tcksum = md5.new(fp.read()).digest()
163             fp.close()
164             if gcksum == tcksum:
165                 os.unlink(os.path.join(mlist.fullpath(), mm_cfg.DEFAULT_SERVER_LANGUAGE, gtemplate))
166         # Match against the templates/* template
167         try:
168             fp = open(os.path.join(mm_cfg.TEMPLATE_DIR, gtemplate))
169         except IOError, e:
170             if e.errno <> errno.ENOENT: raise
171         else:
172             tcksum = md5.new(fp.read()).digest()
173             fp.close()
174             if gcksum == tcksum:
175                 os.unlink(os.path.join(mm_cfg.TEMPLATE_DIR, gtemplate))
176         # Match against the templates/*.prev template
177         try:
178             fp = open(os.path.join(mm_cfg.TEMPLATE_DIR, gtemplate + '.prev'))
179         except IOError, e:
180             if e.errno <> errno.ENOENT: raise
181         else:
182             tcksum = md5.new(fp.read()).digest()
183             fp.close()
184             if gcksum == tcksum:
185                 os.unlink(os.path.join(mm_cfg.TEMPLATE_DIR,
186                                        gtemplate + '.prev'))
187
188
189 \f
190 def dolist(listname):
191     errors = 0
192     mlist = MailList.MailList(listname, lock=0)
193     try:
194         mlist.Lock(0.5)
195     except TimeOutError:
196         print >> sys.stderr, _('WARNING: could not acquire lock for list: '
197                                '%(listname)s')
198         return 1
199
200     # Sanity check the invariant that every BYBOUNCE disabled member must have
201     # bounce information.  Some earlier betas broke this.  BAW: we're
202     # submerging below the MemberAdaptor interface, so skip this if we're not
203     # using OldStyleMemberships.
204     if isinstance(mlist._memberadaptor, OldStyleMemberships):
205         noinfo = {}
206         for addr, (reason, when) in mlist.delivery_status.items():
207             if reason == BYBOUNCE and not mlist.bounce_info.has_key(addr):
208                 noinfo[addr] = reason, when
209         # What to do about these folks with a BYBOUNCE delivery status and no
210         # bounce info?  This number should be very small, and I think it's
211         # fine to simple re-enable them and let the bounce machinery
212         # re-disable them if necessary.
213         n = len(noinfo)
214         if n > 0:
215             print _(
216                 'Resetting %(n)s BYBOUNCEs disabled addrs with no bounce info')
217             for addr in noinfo.keys():
218                 mlist.setDeliveryStatus(addr, ENABLED)
219
220     # Update the held requests database
221     print _("""Updating the held requests database.""")
222     mlist._UpdateRecords()
223
224     mbox_dir = make_varabs('archives/private/%s.mbox' % (listname))
225     mbox_file = make_varabs('archives/private/%s.mbox/%s' % (listname,
226                                                              listname))
227
228     o_pub_mbox_file = make_varabs('archives/public/%s' % (listname))
229     o_pri_mbox_file = make_varabs('archives/private/%s' % (listname))
230
231     html_dir = o_pri_mbox_file
232     o_html_dir = makeabs('public_html/archives/%s' % (listname))
233     #
234     # make the mbox directory if it's not there.
235     #
236     if not os.path.exists(mbox_dir):
237         ou = os.umask(0)
238         os.mkdir(mbox_dir, 02775)
239         os.umask(ou)
240     else:
241         # this shouldn't happen, but hey, just in case
242         if not os.path.isdir(mbox_dir):
243             print _("""\
244 For some reason, %(mbox_dir)s exists as a file.  This won't work with
245 b6, so I'm renaming it to %(mbox_dir)s.tmp and proceeding.""")
246             os.rename(mbox_dir, "%s.tmp" % (mbox_dir))
247             ou = os.umask(0)
248             os.mkdir(mbox_dir, 02775)
249             os.umask(ou)
250
251     # Move any existing mboxes around, but watch out for both a public and a
252     # private one existing
253     if os.path.isfile(o_pri_mbox_file) and os.path.isfile(o_pub_mbox_file):
254         if mlist.archive_private:
255             print _("""\
256
257 %(listname)s has both public and private mbox archives.  Since this list
258 currently uses private archiving, I'm installing the private mbox archive
259 -- %(o_pri_mbox_file)s -- as the active archive, and renaming
260         %(o_pub_mbox_file)s
261 to
262         %(o_pub_mbox_file)s.preb6
263
264 You can integrate that into the archives if you want by using the 'arch'
265 script.
266 """) % (mlist._internal_name, o_pri_mbox_file, o_pub_mbox_file,
267         o_pub_mbox_file)
268             os.rename(o_pub_mbox_file, "%s.preb6" % (o_pub_mbox_file))
269         else:
270             print _("""\
271 %s has both public and private mbox archives.  Since this list
272 currently uses public archiving, I'm installing the public mbox file
273 archive file (%s) as the active one, and renaming
274         %s
275     to
276         %s.preb6
277
278 You can integrate that into the archives if you want by using the 'arch'
279 script.
280 """) % (mlist._internal_name, o_pub_mbox_file, o_pri_mbox_file,
281         o_pri_mbox_file)
282             os.rename(o_pri_mbox_file, "%s.preb6" % (o_pri_mbox_file))
283     #
284     # move private archive mbox there if it's around
285     # and take into account all sorts of absurdities
286     #
287     print _('- updating old private mbox file')
288     if os.path.exists(o_pri_mbox_file):
289         if os.path.isfile(o_pri_mbox_file):
290             os.rename(o_pri_mbox_file, mbox_file)
291         elif not os.path.isdir(o_pri_mbox_file):
292             newname = "%s.mm_install-dunno_what_this_was_but_its_in_the_way" \
293                       % o_pri_mbox_file
294             os.rename(o_pri_mbox_file, newname)
295             print _("""\
296     unknown file in the way, moving
297         %(o_pri_mbox_file)s
298     to
299         %(newname)s""")
300         else:
301             # directory
302             print _("""\
303     Your installation seems up-to-date, great!""")
304
305
306     #
307     # move public archive mbox there if it's around
308     # and take into account all sorts of absurdities.
309     #
310     print _('- updating old public mbox file')
311     if os.path.exists(o_pub_mbox_file):
312         if os.path.isfile(o_pub_mbox_file):
313             os.rename(o_pub_mbox_file, mbox_file)
314         elif not os.path.isdir(o_pub_mbox_file):
315             newname = "%s.mm_install-dunno_what_this_was_but_its_in_the_way" \
316                       % o_pub_mbox_file
317             os.rename(o_pub_mbox_file, newname)
318             print _("""\
319     unknown file in the way, moving
320         %(o_pub_mbox_file)s
321     to
322         %(newname)s""")
323         else: # directory
324             print _("""\
325     Your installation seems up-to-date, great!""")
326
327     #
328     # move the html archives there
329     #
330     if os.path.isdir(o_html_dir):
331         os.rename(o_html_dir, html_dir)
332         #
333         # chmod the html archives
334         #
335         os.chmod(html_dir, 02775)
336     # BAW: Is this still necessary?!
337     mlist.Save()
338     #
339     # Move all the templates to the en language subdirectory as required for
340     # Mailman 2.1
341     #
342     move_language_templates(mlist)
343     # Avoid eating filehandles with the list lockfiles
344     mlist.Unlock()
345     return 0
346
347
348 \f
349 def archive_path_fixer(unused_arg, dir, files):
350     # Passed to os.path.walk to fix the perms on old html archives.
351     for f in files:
352         abs = os.path.join(dir, f)
353         if os.path.isdir(abs):
354             if f == "database":
355                 os.chmod(abs, 02770)
356             else:
357                 os.chmod(abs, 02775)
358         elif os.path.isfile(abs):
359             os.chmod(abs, 0664)
360
361 def remove_old_sources(module):
362     # Also removes old directories.
363     src = '%s/%s' % (mm_cfg.PREFIX, module)
364     pyc = src + "c"
365     if os.path.isdir(src):
366         print _('removing directory %(src)s and everything underneath')
367         shutil.rmtree(src)
368     elif os.path.exists(src):
369         print _('removing %(src)s')
370         try:
371             os.unlink(src)
372         except os.error, rest:
373             print _("Warning: couldn't remove %(src)s -- %(rest)s")
374     if module.endswith('.py') and os.path.exists(pyc):
375         try:
376             os.unlink(pyc)
377         except os.error, rest:
378             print _("couldn't remove old file %(pyc)s -- %(rest)s")
379
380 \f
381 def update_qfiles():
382     print _('updating old qfiles')
383     prefix = `time.time()` + '+'
384     # Be sure the qfiles/in directory exists (we don't really need the
385     # switchboard object, but it's convenient for creating the directory).
386     sb = Switchboard(mm_cfg.INQUEUE_DIR)
387     for filename in os.listdir(mm_cfg.QUEUE_DIR):
388         # Updating means just moving the .db and .msg files to qfiles/in where
389         # it should be dequeued, converted, and processed normally.
390         if filename.endswith('.msg'):
391             oldmsgfile = os.path.join(mm_cfg.QUEUE_DIR, filename)
392             newmsgfile = os.path.join(mm_cfg.INQUEUE_DIR, prefix + filename)
393             os.rename(oldmsgfile, newmsgfile)
394         elif filename.endswith('.db'):
395             olddbfile = os.path.join(mm_cfg.QUEUE_DIR, filename)
396             newdbfile = os.path.join(mm_cfg.INQUEUE_DIR, prefix + filename)
397             os.rename(olddbfile, newdbfile)
398     # Now update for the Mailman 2.1.5 qfile format.  For every filebase in
399     # the qfiles/* directories that has both a .pck and a .db file, pull the
400     # data out and re-queue them.
401     for dirname in os.listdir(mm_cfg.QUEUE_DIR):
402         dirpath = os.path.join(mm_cfg.QUEUE_DIR, dirname)
403         if dirpath == mm_cfg.BADQUEUE_DIR:
404             # The files in qfiles/bad can't possibly be pickles
405             continue
406         sb = Switchboard(dirpath)
407         try:
408             for filename in os.listdir(dirpath):
409                 filepath = os.path.join(dirpath, filename)
410                 filebase, ext = os.path.splitext(filepath)
411                 # Handle the .db metadata files as part of the handling of the
412                 # .pck or .msg message files.
413                 if ext not in ('.pck', '.msg'):
414                     continue
415                 msg, data = dequeue(filebase)
416                 if msg is not None and data is not None:
417                     sb.enqueue(msg, data)
418         except EnvironmentError, e:
419             if e.errno <> errno.ENOTDIR:
420                 raise
421             print _('Warning!  Not a directory: %(dirpath)s')
422
423
424 \f
425 # Implementations taken from the pre-2.1.5 Switchboard
426 def ext_read(filename):
427     fp = open(filename)
428     d = marshal.load(fp)
429     # Update from version 2 files
430     if d.get('version', 0) == 2:
431         del d['filebase']
432     # Do the reverse conversion (repr -> float)
433     for attr in ['received_time']:
434         try:
435             sval = d[attr]
436         except KeyError:
437             pass
438         else:
439             # Do a safe eval by setting up a restricted execution
440             # environment.  This may not be strictly necessary since we
441             # know they are floats, but it can't hurt.
442             d[attr] = eval(sval, {'__builtins__': {}})
443     fp.close()
444     return d
445
446
447 def dequeue(filebase):
448     # Calculate the .db and .msg filenames from the given filebase.
449     msgfile = os.path.join(filebase + '.msg')
450     pckfile = os.path.join(filebase + '.pck')
451     dbfile = os.path.join(filebase + '.db')
452     # Now we are going to read the message and metadata for the given
453     # filebase.  We want to read things in this order: first, the metadata
454     # file to find out whether the message is stored as a pickle or as
455     # plain text.  Second, the actual message file.  However, we want to
456     # first unlink the message file and then the .db file, because the
457     # qrunner only cues off of the .db file
458     msg = None
459     try:
460         data = ext_read(dbfile)
461         os.unlink(dbfile)
462     except EnvironmentError, e:
463         if e.errno <> errno.ENOENT: raise
464         data = {}
465     # Between 2.1b4 and 2.1b5, the `rejection-notice' key in the metadata
466     # was renamed to `rejection_notice', since dashes in the keys are not
467     # supported in METAFMT_ASCII.
468     if data.has_key('rejection-notice'):
469         data['rejection_notice'] = data['rejection-notice']
470         del data['rejection-notice']
471     msgfp = None
472     try:
473         try:
474             msgfp = open(pckfile)
475             msg = cPickle.load(msgfp)
476             os.unlink(pckfile)
477         except EnvironmentError, e:
478             if e.errno <> errno.ENOENT: raise
479             msgfp = None
480             try:
481                 msgfp = open(msgfile)
482                 msg = email.message_from_file(msgfp, Message.Message)
483                 os.unlink(msgfile)
484             except EnvironmentError, e:
485                 if e.errno <> errno.ENOENT: raise
486             except (email.Errors.MessageParseError, ValueError), e:
487                 # This message was unparsable, most likely because its
488                 # MIME encapsulation was broken.  For now, there's not
489                 # much we can do about it.
490                 print _('message is unparsable: %(filebase)s')
491                 msgfp.close()
492                 msgfp = None
493                 if mm_cfg.QRUNNER_SAVE_BAD_MESSAGES:
494                     # Cheapo way to ensure the directory exists w/ the
495                     # proper permissions.
496                     sb = Switchboard(mm_cfg.BADQUEUE_DIR)
497                     os.rename(msgfile, os.path.join(
498                         mm_cfg.BADQUEUE_DIR, filebase + '.txt'))
499                 else:
500                     os.unlink(msgfile)
501                 msg = data = None
502         except EOFError:
503             # For some reason the pckfile was empty.  Just delete it.
504             print _('Warning!  Deleting empty .pck file: %(pckfile)s')
505             os.unlink(pckfile)
506     finally:
507         if msgfp:
508             msgfp.close()
509     return msg, data
510
511
512 \f
513 def update_pending():
514     file20 = os.path.join(mm_cfg.DATA_DIR, 'pending_subscriptions.db')
515     file214 = os.path.join(mm_cfg.DATA_DIR, 'pending.pck')
516     db = None
517     ver = None
518     # Try to load the Mailman 2.0 file
519     try:
520         fp = open(file20)
521         ver = "20"
522     except IOError, e:
523         if e.errno <> errno.ENOENT: raise
524     else:
525         print _('Updating Mailman 2.0 pending_subscriptions.db database')
526         db = marshal.load(fp)
527         # Convert to the pre-Mailman 2.1.5 format
528         db = Pending._update(db)
529     if db is None:
530         # Try to load the Mailman 2.1.x where x < 5, file
531         try:
532             fp = open(file214)
533             ver = "214"
534         except IOError, e:
535             if e.errno <> errno.ENOENT: raise
536         else:
537             print _('Updating Mailman 2.1.4 pending.pck database')
538             db = cPickle.load(fp)
539     if db is None:
540         print _('Nothing to do.')
541         return
542     # Now upgrade the database to the 2.1.5 format.  Each list now has its own
543     # pending.pck file, but only the RE_ENABLE operation actually recorded the
544     # listname in the request.  For the SUBSCRIPTION, UNSUBSCRIPTION, and
545     # CHANGE_OF_ADDRESS operations, we know the address of the person making
546     # the request so we can repend this request just for the lists the person
547     # is a member of.  For the HELD_MESSAGE operation, we can check the list's
548     # requests.pck file for correlation.  Evictions will take care of any
549     # misdirected pendings.
550     reenables_by_list = {}
551     addrops_by_address = {}
552     holds_by_id = {}
553     subs_by_address = {}
554     for key, val in db.items():
555         if key in ('evictions', 'version'):
556             continue
557         try:
558             op = val[0]
559             data = val[1:]
560         except (IndexError, ValueError):
561             print _('Ignoring bad pended data: %(key)s: %(val)s')
562             continue
563         if op in (Pending.UNSUBSCRIPTION, Pending.CHANGE_OF_ADDRESS):
564             # data[0] is the address being unsubscribed
565             addrops_by_address.setdefault(data[0], []).append((key, val))
566         elif op == Pending.SUBSCRIPTION:
567             if ver == "20":
568                 # data is tuple (emailaddr, password, digest)
569                 addr = data[0]
570             else:
571                 # data[0] is a UserDesc object
572                 addr = data[0].address
573             subs_by_address.setdefault(addr, []).append((key, val))
574         elif op == Pending.RE_ENABLE:
575             # data[0] is the mailing list's internal name
576             reenables_by_list.setdefault(data[0], []).append((key, val))
577         elif op == Pending.HELD_MESSAGE:
578             # data[0] is the hold id.  There better only be one entry per id
579             id = data[0]
580             if holds_by_id.has_key(id):
581                 print _('WARNING: Ignoring duplicate pending ID: %(id)s.')
582             else:
583                 holds_by_id[id] = (key, val)
584     # Now we have to lock every list and re-pend all the appropriate
585     # requests.  Note that this will reset all the expiration dates, but that
586     # should be fine.
587     for listname in Utils.list_names():
588         mlist = MailList.MailList(listname)
589         # This is not the most efficient way to do this because it loads and
590         # saves the pending.pck file each time. :(
591         try:
592             for cookie, data in reenables_by_list.get(listname, []):
593                 mlist.pend_repend(cookie, data)
594             for id, (cookie, data) in holds_by_id.items():
595                 try:
596                     rec = mlist.GetRecord(id)
597                 except KeyError:
598                     # Not for this list
599                     pass
600                 else:
601                     mlist.pend_repend(cookie, data)
602                     del holds_by_id[id]
603             for addr, recs in subs_by_address.items():
604                 # We shouldn't have a subscription confirmation if the address
605                 # is already a member of the mailing list.
606                 if mlist.isMember(addr):
607                     continue
608                 for cookie, data in recs:
609                     mlist.pend_repend(cookie, data)
610             for addr, recs in addrops_by_address.items():
611                 # We shouldn't have unsubscriptions or change of address
612                 # requests for addresses which aren't members of the list.
613                 if not mlist.isMember(addr):
614                     continue
615                 for cookie, data in recs:
616                     mlist.pend_repend(cookie, data)
617             mlist.Save()
618         finally:
619             mlist.Unlock()
620     try:
621         os.unlink(file20)
622     except OSError, e:
623         if e.errno <> errno.ENOENT: raise
624     try:
625         os.unlink(file214)
626     except OSError, e:
627         if e.errno <> errno.ENOENT: raise
628
629
630 \f
631 def main():
632     errors = 0
633     # get rid of old stuff
634     print _('getting rid of old source files')
635     for mod in ('Mailman/Archiver.py', 'Mailman/HyperArch.py',
636                 'Mailman/HyperDatabase.py', 'Mailman/pipermail.py',
637                 'Mailman/smtplib.py', 'Mailman/Cookie.py',
638                 'bin/update_to_10b6', 'scripts/mailcmd',
639                 'scripts/mailowner', 'Mailman/pythonlib',
640                 'cgi-bin/archives', 'Mailman/MailCommandHandler'):
641         remove_old_sources(mod)
642     listnames = Utils.list_names()
643     if not listnames:
644 #        print _('no lists == nothing to do, exiting')
645         return
646     #
647     # for people with web archiving, make sure the directories
648     # in the archiving are set with proper perms for b6.
649     #
650     if os.path.isdir("%s/public_html/archives" % mm_cfg.PREFIX):
651         print _("""\
652 fixing all the perms on your old html archives to work with b6
653 If your archives are big, this could take a minute or two...""")
654         os.path.walk("%s/public_html/archives" % mm_cfg.PREFIX,
655                      archive_path_fixer, "")
656         print _('done')
657     for listname in listnames:
658         print _('Updating mailing list: %(listname)s')
659         errors = errors + dolist(listname)
660         print
661     print _('Updating Usenet watermarks')
662     wmfile = os.path.join(mm_cfg.DATA_DIR, 'gate_watermarks')
663     try:
664         fp = open(wmfile)
665     except IOError:
666         print _('- nothing to update here')
667     else:
668         d = marshal.load(fp)
669         fp.close()
670         for listname in d.keys():
671             if listname not in listnames:
672                 # this list no longer exists
673                 continue
674             mlist = MailList.MailList(listname, lock=0)
675             try:
676                 mlist.Lock(0.5)
677             except TimeOutError:
678                 print >> sys.stderr, _(
679                     'WARNING: could not acquire lock for list: %(listname)s')
680                 errors = errors + 1
681             else:
682                 # Pre 1.0b7 stored 0 in the gate_watermarks file to indicate
683                 # that no gating had been done yet.  Without coercing this to
684                 # None, the list could now suddenly get flooded.
685                 mlist.usenet_watermark = d[listname] or None
686                 mlist.Save()
687                 mlist.Unlock()
688         os.unlink(wmfile)
689         print _('- usenet watermarks updated and gate_watermarks removed')
690     # In Mailman 2.1, the pending database format and file name changed, but
691     # in Mailman 2.1.5 it changed again.  This should update all existing
692     # files to the 2.1.5 format.
693     update_pending()
694     # In Mailman 2.1, the qfiles directory has a different structure and a
695     # different content.  Also, in Mailman 2.1.5 we collapsed the message
696     # files from separate .msg (pickled Message objects) and .db (marshalled
697     # dictionaries) to a shared .pck file containing two pickles.
698     update_qfiles()
699     # This warning was necessary for the upgrade from 1.0b9 to 1.0b10.
700     # There's no good way of figuring this out for releases prior to 2.0beta2
701     # :(
702     if lastversion == NOTFRESH:
703         print _("""
704
705 NOTE NOTE NOTE NOTE NOTE
706
707     You are upgrading an existing Mailman installation, but I can't tell what
708     version you were previously running.
709
710     If you are upgrading from Mailman 1.0b9 or earlier you will need to
711     manually update your mailing lists.  For each mailing list you need to
712     copy the file templates/options.html lists/<listname>/options.html.
713
714     However, if you have edited this file via the Web interface, you will have
715     to merge your changes into this file, otherwise you will lose your
716     changes.
717
718 NOTE NOTE NOTE NOTE NOTE
719
720 """)
721     return errors
722
723
724 \f
725 def usage(code, msg=''):
726     if code:
727         fd = sys.stderr
728     else:
729         fd = sys.stdout
730     print >> fd, _(__doc__) % globals()
731     if msg:
732         print >> sys.stderr, msg
733     sys.exit(code)
734
735
736 \f
737 if __name__ == '__main__':
738     try:
739         opts, args = getopt.getopt(sys.argv[1:], 'hf',
740                                    ['help', 'force'])
741     except getopt.error, msg:
742         usage(1, msg)
743
744     if args:
745         usage(1, 'Unexpected arguments: %s' % args)
746
747     force = 0
748     for opt, arg in opts:
749         if opt in ('-h', '--help'):
750             usage(0)
751         elif opt in ('-f', '--force'):
752             force = 1
753
754     # calculate the versions
755     lastversion, thisversion = calcversions()
756     hexlversion = hex(lastversion)
757     hextversion = hex(thisversion)
758     if lastversion == thisversion and not force:
759         # nothing to do
760         print _('No updates are necessary.')
761         sys.exit(0)
762     if lastversion > thisversion and not force:
763         print _("""\
764 Downgrade detected, from version %(hexlversion)s to version %(hextversion)s
765 This is probably not safe.
766 Exiting.""")
767         sys.exit(1)
768     print _('Upgrading from version %(hexlversion)s to %(hextversion)s')
769     errors = main()
770     if not errors:
771         # Record the version we just upgraded to
772         fp = open(LMVFILE, 'w')
773         fp.write(hex(mm_cfg.HEX_VERSION) + '\n')
774         fp.close()
775     else:
776         lockdir = mm_cfg.LOCK_DIR
777         print _('''\
778
779 ERROR:
780
781 The locks for some lists could not be acquired.  This means that either
782 Mailman was still active when you upgraded, or there were stale locks in the
783 %(lockdir)s directory.
784
785 You must put Mailman into a quiescent state and remove all stale locks, then
786 re-run "make update" manually.  See the INSTALL and UPGRADE files for details.
787 ''')