Apply 79_archiver_slash.patch
[mspang/vmailman.git] / Mailman / Archiver / Archiver.py
1 # Copyright (C) 1998-2003 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, USA.
16
17
18 """Mixin class for putting new messages in the right place for archival.
19
20 Public archives are separated from private ones.  An external archival
21 mechanism (eg, pipermail) should be pointed to the right places, to do the
22 archival.
23 """
24
25 import os
26 import errno
27 import traceback
28 import re
29 from cStringIO import StringIO
30
31 from Mailman import mm_cfg
32 from Mailman import Mailbox
33 from Mailman import Utils
34 from Mailman import Site
35 from Mailman.SafeDict import SafeDict
36 from Mailman.Logging.Syslog import syslog
37 from Mailman.i18n import _
38
39 try:
40     True, False
41 except NameError:
42     True = 1
43     False = 0
44
45
46 \f
47 def makelink(old, new):
48     try:
49         os.symlink(old, new)
50     except OSError, e:
51         if e.errno <> errno.EEXIST:
52             raise
53
54 def breaklink(link):
55     try:
56         os.unlink(link)
57     except OSError, e:
58         if e.errno <> errno.ENOENT:
59             raise
60
61
62 \f
63 class Archiver:
64     #
65     # Interface to Pipermail.  HyperArch.py uses this method to get the
66     # archive directory for the mailing list
67     #
68     def InitVars(self):
69         # Configurable
70         self.archive = mm_cfg.DEFAULT_ARCHIVE
71         # 0=public, 1=private:
72         self.archive_private = mm_cfg.DEFAULT_ARCHIVE_PRIVATE
73         self.archive_volume_frequency = \
74                 mm_cfg.DEFAULT_ARCHIVE_VOLUME_FREQUENCY
75         # The archive file structure by default is:
76         #
77         # archives/
78         #     private/
79         #         listname.mbox/
80         #             listname.mbox
81         #         listname/
82         #             lots-of-pipermail-stuff
83         #     public/
84         #         listname.mbox@ -> ../private/listname.mbox
85         #         listname@ -> ../private/listname
86         #
87         # IOW, the mbox and pipermail archives are always stored in the
88         # private archive for the list.  This is safe because archives/private
89         # is always set to o-rx.  Public archives have a symlink to get around
90         # the private directory, pointing directly to the private/listname
91         # which has o+rx permissions.  Private archives do not have the
92         # symbolic links.
93         omask = os.umask(0)
94         try:
95             try:
96                 os.mkdir(self.archive_dir()+'.mbox', 02775)
97             except OSError, e:
98                 if e.errno <> errno.EEXIST: raise
99                 # We also create an empty pipermail archive directory into
100                 # which we'll drop an empty index.html file into.  This is so
101                 # that lists that have not yet received a posting have
102                 # /something/ as their index.html, and don't just get a 404.
103             try:
104                 os.mkdir(self.archive_dir(), 02775)
105             except OSError, e:
106                 if e.errno <> errno.EEXIST: raise
107             # See if there's an index.html file there already and if not,
108             # write in the empty archive notice.
109             indexfile = os.path.join(self.archive_dir(), 'index.html')
110             fp = None
111             try:
112                 fp = open(indexfile)
113             except IOError, e:
114                 if e.errno <> errno.ENOENT: raise
115                 omask = os.umask(002)
116                 try:
117                     fp = open(indexfile, 'w')
118                 finally:
119                     os.umask(omask)
120                 fp.write(Utils.maketext(
121                     'emptyarchive.html',
122                     {'listname': self.real_name,
123                      'listinfo': self.GetScriptURL('listinfo', absolute=1),
124                      }, mlist=self))
125             if fp:
126                 fp.close()
127         finally:
128             os.umask(omask)
129
130     def archive_dir(self):
131         return Site.get_archpath(self.internal_name())
132
133     def ArchiveFileName(self):
134         """The mbox name where messages are left for archive construction."""
135         return os.path.join(self.archive_dir() + '.mbox',
136                             self.internal_name() + '.mbox')
137
138     def GetBaseArchiveURL(self):
139         url = self.GetScriptURL('private', absolute=1) + '/'
140         if self.archive_private:
141             return url
142         else:
143             hostname = re.match('[^:]*://([^/]*)/.*', url).group(1)\
144                        or mm_cfg.DEFAULT_URL_HOST
145             url = mm_cfg.PUBLIC_ARCHIVE_URL % {
146                 'listname': self.internal_name(),
147                 'hostname': hostname
148                 }
149             return url
150
151     def __archive_file(self, afn):
152         """Open (creating, if necessary) the named archive file."""
153         omask = os.umask(002)
154         try:
155             return Mailbox.Mailbox(open(afn, 'a+'))
156         finally:
157             os.umask(omask)
158
159     #
160     # old ArchiveMail function, retained under a new name
161     # for optional archiving to an mbox
162     #
163     def __archive_to_mbox(self, post):
164         """Retain a text copy of the message in an mbox file."""
165         try:
166             afn = self.ArchiveFileName()
167             mbox = self.__archive_file(afn)
168             mbox.AppendMessage(post)
169             mbox.fp.close()
170         except IOError, msg:
171             syslog('error', 'Archive file access failure:\n\t%s %s', afn, msg)
172             raise
173
174     def ExternalArchive(self, ar, txt):
175         d = SafeDict({'listname': self.internal_name(),
176                       'hostname': self.host_name,
177                       })
178         cmd = ar % d
179         extarch = os.popen(cmd, 'w')
180         extarch.write(txt)
181         status = extarch.close()
182         if status:
183             syslog('error', 'external archiver non-zero exit status: %d\n',
184                    (status & 0xff00) >> 8)
185
186     #
187     # archiving in real time  this is called from list.post(msg)
188     #
189     def ArchiveMail(self, msg):
190         """Store postings in mbox and/or pipermail archive, depending."""
191         # Fork so archival errors won't disrupt normal list delivery
192         if mm_cfg.ARCHIVE_TO_MBOX == -1:
193             return
194         #
195         # We don't need an extra archiver lock here because we know the list
196         # itself must be locked.
197         if mm_cfg.ARCHIVE_TO_MBOX in (1, 2):
198             self.__archive_to_mbox(msg)
199             if mm_cfg.ARCHIVE_TO_MBOX == 1:
200                 # Archive to mbox only.
201                 return
202         txt = str(msg)
203         # should we use the internal or external archiver?
204         private_p = self.archive_private
205         if mm_cfg.PUBLIC_EXTERNAL_ARCHIVER and not private_p:
206             self.ExternalArchive(mm_cfg.PUBLIC_EXTERNAL_ARCHIVER, txt)
207         elif mm_cfg.PRIVATE_EXTERNAL_ARCHIVER and private_p:
208             self.ExternalArchive(mm_cfg.PRIVATE_EXTERNAL_ARCHIVER, txt)
209         else:
210             # use the internal archiver
211             f = StringIO(txt)
212             import HyperArch
213             h = HyperArch.HyperArchive(self)
214             h.processUnixMailbox(f)
215             h.close()
216             f.close()
217
218     #
219     # called from MailList.MailList.Save()
220     #
221     def CheckHTMLArchiveDir(self):
222         # We need to make sure that the archive directory has the right perms
223         # for public vs private.  If it doesn't exist, or some weird
224         # permissions errors prevent us from stating the directory, it's
225         # pointless to try to fix the perms, so we just return -scott
226         if mm_cfg.ARCHIVE_TO_MBOX == -1:
227             # Archiving is completely disabled, don't require the skeleton.
228             return
229         pubdir = Site.get_archpath(self.internal_name(), public=True)
230         privdir = self.archive_dir()
231         pubmbox = pubdir + '.mbox'
232         privmbox = privdir + '.mbox'
233         if self.archive_private:
234             breaklink(pubdir)
235             breaklink(pubmbox)
236         else:
237             # BAW: privdir or privmbox could be nonexistant.  We'd get an
238             # OSError, ENOENT which should be caught and reported properly.
239             makelink(privdir, pubdir)
240             # Only make this link if the site has enabled public mbox files
241             if mm_cfg.PUBLIC_MBOX:
242                 makelink(privmbox, pubmbox)