[a-zA-Z]+)$',
'week': r'^Week-of-Mon-' + yre + mre + dre,
'day': '^' + yre + mre + dre + '$'
}
def _makeArticle(self, msg, sequence):
return Article(msg, sequence,
lang=self.maillist.preferred_language,
mlist=self.maillist)
def html_foot(self):
mlist = self.maillist
# Convenience
def quotetime(s):
return html_quote(ctime(s), self.lang.code)
# Avoid i18n side-effects
with _.using(mlist.preferred_language.code):
d = {"lastdate": quotetime(self.lastdate),
"archivedate": quotetime(self.archivedate),
"listinfo": mlist.script_url('listinfo'),
"version": self.version,
}
i = {"thread": _("thread"),
"subject": _("subject"),
"author": _("author"),
"date": _("date")
}
for t in i.keys():
cap = t[0].upper() + t[1:]
if self.type == cap:
d["%s_ref" % (t)] = ""
else:
d["%s_ref" % (t)] = ('[ %s ]'
% (t, i[t]))
return quick_maketext(
'archidxfoot.html', d,
mlist=mlist)
def html_head(self):
mlist = self.maillist
# Convenience
def quotetime(s):
return html_quote(ctime(s), self.lang.code)
# Avoid i18n side-effects
with _.using(mlist.preferred_language.code):
d = {"listname": html_quote(mlist.real_name, self.lang.code),
"archtype": self.type,
"archive": self.volNameToDesc(self.archive),
"listinfo": mlist.script_url('listinfo'),
"firstdate": quotetime(self.firstdate),
"lastdate": quotetime(self.lastdate),
"size": self.size,
}
i = {"thread": _("thread"),
"subject": _("subject"),
"author": _("author"),
"date": _("date"),
}
for t in i.keys():
cap = t[0].upper() + t[1:]
if self.type == cap:
d["%s_ref" % (t)] = ""
d["archtype"] = i[t]
else:
d["%s_ref" % (t)] = ('[ %s ]'
% (t, i[t]))
if self.charset:
d["encoding"] = html_charset % self.charset
else:
d["encoding"] = ""
return quick_maketext(
'archidxhead.html', d,
mlist=mlist)
def html_TOC(self):
mlist = self.maillist
listname = mlist.fqdn_listname
mbox = os.path.join(mlist.archive_dir()+'.mbox', listname+'.mbox')
d = {"listname": mlist.real_name,
"listinfo": mlist.script_url('listinfo'),
"fullarch": '../%s.mbox/%s.mbox' % (listname, listname),
"size": sizeof(mbox, mlist.preferred_language),
'meta': '',
}
# Avoid i18n side-effects
with _.using(mlist.preferred_language.code):
if not self.archives:
d["noarchive_msg"] = _(
'Currently, there are no archives.
')
d["archive_listing_start"] = ""
d["archive_listing_end"] = ""
d["archive_listing"] = ""
else:
d["noarchive_msg"] = ""
d["archive_listing_start"] = quick_maketext(
'archliststart.html',
lang=mlist.preferred_language,
mlist=mlist)
d["archive_listing_end"] = quick_maketext(
'archlistend.html',
mlist=mlist)
accum = []
for a in self.archives:
accum.append(self.html_TOC_entry(a))
d["archive_listing"] = EMPTYSTRING.join(accum)
# The TOC is always in the charset of the list's preferred language
d['meta'] += html_charset % mlist.preferred_language.charset
# The site can disable public access to the mbox file.
if as_boolean(config.archiver.pipermail.public_mbox):
template = 'archtoc.html'
else:
template = 'archtocnombox.html'
return quick_maketext(template, d, mlist=mlist)
def html_TOC_entry(self, arch):
# Check to see if the archive is gzip'd or not
txtfile = os.path.join(self.maillist.archive_dir(), arch + '.txt')
gzfile = txtfile + '.gz'
# which exists? .txt.gz first, then .txt
if os.path.exists(gzfile):
file = gzfile
url = arch + '.txt.gz'
templ = '[ ' + _('Gzip\'d Text%(sz)s') \
+ '] | '
elif os.path.exists(txtfile):
file = txtfile
url = arch + '.txt'
templ = '[ ' + _('Text%(sz)s') + '] | '
else:
# neither found?
file = None
# in Python 1.5.2 we have an easy way to get the size
if file:
textlink = templ % {
'url': url,
'sz' : sizeof(file, self.maillist.preferred_language)
}
else:
# there's no archive file at all... hmmm.
textlink = ''
return quick_maketext(
'archtocentry.html',
{'archive': arch,
'archivelabel': self.volNameToDesc(arch),
'textlink': textlink
},
mlist=self.maillist)
def GetArchLock(self):
if self._lock_file:
return 1
self._lock_file = Lock(
os.path.join(config.LOCK_DIR,
self.maillist.fqdn_listname + '-arch.lock'))
try:
self._lock_file.lock(timeout=0.5)
except TimeOutError:
return 0
return 1
def DropArchLock(self):
if self._lock_file:
self._lock_file.unlock(unconditionally=1)
self._lock_file = None
def processListArch(self):
name = self.maillist.ArchiveFileName()
wname= name+'.working'
ename= name+'.err_unarchived'
try:
os.stat(name)
except (IOError,os.error):
#no archive file, nothin to do -ddm
return
#see if arch is locked here -ddm
if not self.GetArchLock():
#another archiver is running, nothing to do. -ddm
return
#if the working file is still here, the archiver may have
# crashed during archiving. Save it, log an error, and move on.
try:
wf = open(wname)
log.error('Archive working file %s present. '
'Check %s for possibly unarchived msgs',
wname, ename)
omask = os.umask(007)
try:
ef = open(ename, 'a+')
finally:
os.umask(omask)
ef.seek(1,2)
if ef.read(1) <> '\n':
ef.write('\n')
ef.write(wf.read())
ef.close()
wf.close()
os.unlink(wname)
except IOError:
pass
os.rename(name,wname)
archfile = open(wname)
self.processUnixMailbox(archfile)
archfile.close()
os.unlink(wname)
self.DropArchLock()
def get_filename(self, article):
return '%06i.html' % (article.sequence,)
def get_archives(self, article):
"""Return a list of indexes where the article should be filed.
A string can be returned if the list only contains one entry,
and the empty list is legal."""
res = self.dateToVolName(float(article.date))
self.message(_("figuring article archives\n"))
self.message(res + "\n")
return res
def volNameToDesc(self, volname):
volname = volname.strip()
# Don't make these module global constants since we have to runtime
# translate them anyway.
monthdict = [
'',
_('January'), _('February'), _('March'), _('April'),
_('May'), _('June'), _('July'), _('August'),
_('September'), _('October'), _('November'), _('December')
]
for each in self._volre.keys():
match = re.match(self._volre[each], volname)
# Let ValueErrors percolate up
if match:
year = int(match.group('year'))
if each == 'quarter':
d =["", _("First"), _("Second"), _("Third"), _("Fourth") ]
ord = d[int(match.group('quarter'))]
return _("%(ord)s quarter %(year)i")
elif each == 'month':
monthstr = match.group('month').lower()
for i in range(1, 13):
monthname = time.strftime("%B", (1999,i,1,0,0,0,0,1,0))
if monthstr.lower() == monthname.lower():
month = monthdict[i]
return _("%(month)s %(year)i")
raise ValueError, "%s is not a month!" % monthstr
elif each == 'week':
month = monthdict[int(match.group("month"))]
day = int(match.group("day"))
return _("The Week Of Monday %(day)i %(month)s %(year)i")
elif each == 'day':
month = monthdict[int(match.group("month"))]
day = int(match.group("day"))
return _("%(day)i %(month)s %(year)i")
else:
return match.group('year')
raise ValueError, "%s is not a valid volname" % volname
# The following two methods should be inverses of each other. -ddm
def dateToVolName(self,date):
datetuple=time.localtime(date)
if self.ARCHIVE_PERIOD=='year':
return time.strftime("%Y",datetuple)
elif self.ARCHIVE_PERIOD=='quarter':
if datetuple[1] in [1,2,3]:
return time.strftime("%Yq1",datetuple)
elif datetuple[1] in [4,5,6]:
return time.strftime("%Yq2",datetuple)
elif datetuple[1] in [7,8,9]:
return time.strftime("%Yq3",datetuple)
else:
return time.strftime("%Yq4",datetuple)
elif self.ARCHIVE_PERIOD == 'day':
return time.strftime("%Y%m%d", datetuple)
elif self.ARCHIVE_PERIOD == 'week':
# Reconstruct "seconds since epoch", and subtract weekday
# multiplied by the number of seconds in a day.
monday = time.mktime(datetuple) - datetuple[6] * 24 * 60 * 60
# Build a new datetuple from this "seconds since epoch" value
datetuple = time.localtime(monday)
return time.strftime("Week-of-Mon-%Y%m%d", datetuple)
# month. -ddm
else:
return time.strftime("%Y-%B",datetuple)
def volNameToDate(self, volname):
volname = volname.strip()
for each in self._volre.keys():
match = re.match(self._volre[each],volname)
if match:
year = int(match.group('year'))
month = 1
day = 1
if each == 'quarter':
q = int(match.group('quarter'))
month = (q * 3) - 2
elif each == 'month':
monthstr = match.group('month').lower()
m = []
for i in range(1,13):
m.append(
time.strftime("%B",(1999,i,1,0,0,0,0,1,0)).lower())
try:
month = m.index(monthstr) + 1
except ValueError:
pass
elif each == 'week' or each == 'day':
month = int(match.group("month"))
day = int(match.group("day"))
try:
return time.mktime((year,month,1,0,0,0,0,1,-1))
except OverflowError:
return 0.0
return 0.0
def sortarchives(self):
def sf(a, b):
al = self.volNameToDate(a)
bl = self.volNameToDate(b)
if al > bl:
return 1
elif al < bl:
return -1
else:
return 0
if self.ARCHIVE_PERIOD in ('month','year','quarter'):
self.archives.sort(sf)
else:
self.archives.sort()
self.archives.reverse()
def message(self, msg):
if self.VERBOSE:
f = sys.stderr
f.write(msg)
if msg[-1:] != '\n':
f.write('\n')
f.flush()
def open_new_archive(self, archive, archivedir):
index_html = os.path.join(archivedir, 'index.html')
try:
os.unlink(index_html)
except:
pass
os.symlink(self.DEFAULTINDEX+'.html',index_html)
def write_index_header(self):
self.depth=0
print self.html_head()
if not self.THREADLAZY and self.type=='Thread':
self.message(_("Computing threaded index\n"))
self.updateThreadedIndex()
def write_index_footer(self):
for i in range(self.depth):
print ''
print self.html_foot()
def write_index_entry(self, article):
subject = self.get_header("subject", article)
author = self.get_header("author", article)
if as_boolean(config.archiver.pipermail.obscure_email_addresses):
try:
author = re.sub('@', _(' at '), author)
except UnicodeError:
# Non-ASCII author contains '@' ... no valid email anyway
pass
subject = CGIescape(subject, self.lang)
author = CGIescape(author, self.lang)
d = {
'filename': urllib.quote(article.filename),
'subject': subject,
'sequence': article.sequence,
'author': author
}
print quick_maketext(
'archidxentry.html', d,
mlist=self.maillist)
def get_header(self, field, article):
# if we have no decoded header, return the encoded one
result = article.decoded.get(field)
if result is None:
return getattr(article, field)
# otherwise, the decoded one will be Unicode
return result
def write_threadindex_entry(self, article, depth):
if depth < 0:
self.message('depth<0')
depth = 0
if depth > self.THREADLEVELS:
depth = self.THREADLEVELS
if depth < self.depth:
for i in range(self.depth-depth):
print ''
elif depth > self.depth:
for i in range(depth-self.depth):
print ''
print '' % (depth, article.threadKey)
self.depth = depth
self.write_index_entry(article)
def write_TOC(self):
self.sortarchives()
omask = os.umask(002)
try:
toc = open(os.path.join(self.basedir, 'index.html'), 'w')
finally:
os.umask(omask)
toc.write(self.html_TOC())
toc.close()
def write_article(self, index, article, path):
# called by add_article
omask = os.umask(002)
try:
f = open(path, 'w')
finally:
os.umask(omask)
f.write(article.as_html())
f.close()
# Write the text article to the text archive.
path = os.path.join(self.basedir, "%s.txt" % index)
omask = os.umask(002)
try:
f = open(path, 'a+')
finally:
os.umask(omask)
f.write(article.as_text())
f.close()
def update_archive(self, archive):
self.__super_update_archive(archive)
# only do this if the gzip module was imported globally, and
# gzip'ing was enabled via Defaults.GZIP_ARCHIVE_TXT_FILES. See
# above.
if gzip:
archz = None
archt = None
txtfile = os.path.join(self.basedir, '%s.txt' % archive)
gzipfile = os.path.join(self.basedir, '%s.txt.gz' % archive)
oldgzip = os.path.join(self.basedir, '%s.old.txt.gz' % archive)
try:
# open the plain text file
archt = open(txtfile)
except IOError:
return
try:
os.rename(gzipfile, oldgzip)
archz = gzip.open(oldgzip)
except (IOError, RuntimeError, os.error):
pass
try:
ou = os.umask(002)
newz = gzip.open(gzipfile, 'w')
finally:
# XXX why is this a finally?
os.umask(ou)
if archz:
newz.write(archz.read())
archz.close()
os.unlink(oldgzip)
# XXX do we really need all this in a try/except?
try:
newz.write(archt.read())
newz.close()
archt.close()
except IOError:
pass
os.unlink(txtfile)
_skip_attrs = ('maillist', '_lock_file', 'charset')
def getstate(self):
d={}
for each in self.__dict__.keys():
if not (each in self._skip_attrs
or each.upper() == each):
d[each] = self.__dict__[each]
return d
# Add tags around URLs and e-mail addresses.
def __processbody_URLquote(self, lines):
# XXX a lot to do here:
# 1. use lines directly, rather than source and dest
# 2. make it clearer
# 3. make it faster
# TK: Prepare for unicode obscure.
atmark = _(' at ')
if lines and isinstance(lines[0], unicode):
atmark = unicode(atmark, self.lang.charset, 'replace')
source = lines[:]
dest = lines
last_line_was_quoted = 0
for i in xrange(0, len(source)):
Lorig = L = source[i]
prefix = suffix = ""
if L is None:
continue
# Italicise quoted text
if self.IQUOTES:
quoted = quotedpat.match(L)
if quoted is None:
last_line_was_quoted = 0
else:
quoted = quoted.end(0)
prefix = CGIescape(L[:quoted], self.lang) + ''
suffix = ''
if self.SHOWHTML:
suffix += '
'
if not last_line_was_quoted:
prefix = '
' + prefix
L = L[quoted:]
last_line_was_quoted = 1
# Check for an e-mail address
L2 = ""
jr = emailpat.search(L)
kr = urlpat.search(L)
while jr is not None or kr is not None:
if jr == None:
j = -1
else:
j = jr.start(0)
if kr is None:
k = -1
else:
k = kr.start(0)
if j != -1 and (j < k or k == -1):
text = jr.group(1)
length = len(text)
if as_boolean(
config.archiver.pipermail.obscure_email_addresses):
text = re.sub('@', atmark, text)
URL = self.maillist.script_url('listinfo')
else:
URL = 'mailto:' + text
pos = j
elif k != -1 and (j > k or j == -1):
text = URL = kr.group(1)
length = len(text)
pos = k
else: # j==k
raise ValueError, "j==k: This can't happen!"
#length = len(text)
#self.message("URL: %s %s %s \n"
# % (CGIescape(L[:pos]), URL, CGIescape(text)))
L2 += '%s%s' % (
CGIescape(L[:pos], self.lang),
html_quote(URL), CGIescape(text, self.lang))
L = L[pos+length:]
jr = emailpat.search(L)
kr = urlpat.search(L)
if jr is None and kr is None:
L = CGIescape(L, self.lang)
L = prefix + L2 + L + suffix
source[i] = None
dest[i] = L
# Perform Hypermail-style processing of directives
# in message bodies. Lines between and will be written
# out precisely as they are; other lines will be passed to func2
# for further processing .
def __processbody_HTML(self, lines):
# XXX need to make this method modify in place
source = lines[:]
dest = lines
l = len(source)
i = 0
while i < l:
while i < l and htmlpat.match(source[i]) is None:
i = i + 1
if i < l:
source[i] = None
i = i + 1
while i < l and nohtmlpat.match(source[i]) is None:
dest[i], source[i] = source[i], None
i = i + 1
if i < l:
source[i] = None
i = i + 1
def format_article(self, article):
# called from add_article
# TBD: Why do the HTML formatting here and keep it in the
# pipermail database? It makes more sense to do the html
# formatting as the article is being written as html and toss
# the data after it has been written to the archive file.
lines = filter(None, article.body)
# Handle directives
if self.ALLOWHTML:
self.__processbody_HTML(lines)
self.__processbody_URLquote(lines)
if not self.SHOWHTML and lines:
lines.insert(0, '
')
lines.append('')
else:
# Do fancy formatting here
if self.SHOWBR:
lines = map(lambda x:x + "
", lines)
else:
for i in range(0, len(lines)):
s = lines[i]
if s[0:1] in ' \t\n':
lines[i] = '' + s
article.html_body = lines
return article
def update_article(self, arcdir, article, prev, next):
seq = article.sequence
filename = os.path.join(arcdir, article.filename)
self.message(_('Updating HTML for article %(seq)s'))
try:
f = open(filename)
article.loadbody_fromHTML(f)
f.close()
except IOError, e:
if e.errno <> errno.ENOENT: raise
self.message(_('article file %(filename)s is missing!'))
article.prev = prev
article.next = next
omask = os.umask(002)
try:
f = open(filename, 'w')
finally:
os.umask(omask)
f.write(article.as_html())
f.close()