Port header matching previously described by the misnamed KNONW_SPAMMERS
[mailman.git] / Mailman / HTMLFormatter.py
blob8fa650ed77d0eeead72f3b2b48146251466374aa
1 # Copyright (C) 1998-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,
16 # USA.
18 """Routines for presentation of list-specific HTML text."""
20 import re
21 import time
23 from Mailman import Defaults
24 from Mailman import MemberAdaptor
25 from Mailman import Utils
26 from Mailman.configuration import config
27 from Mailman.htmlformat import *
28 from Mailman.i18n import _
31 EMPTYSTRING = ''
32 BR = '<br>'
33 NL = '\n'
34 COMMASPACE = ', '
38 class HTMLFormatter:
39 def GetMailmanFooter(self):
40 ownertext = COMMASPACE.join([Utils.ObscureEmail(a, 1)
41 for a in self.owner])
42 # Remove the .Format() when htmlformat conversion is done.
43 realname = self.real_name
44 hostname = self.host_name
45 listinfo_link = Link(self.GetScriptURL('listinfo'),
46 realname).Format()
47 owner_link = Link('mailto:' + self.GetOwnerEmail(), ownertext).Format()
48 innertext = _('%(listinfo_link)s list run by %(owner_link)s')
49 return Container(
50 '<hr>',
51 Address(
52 Container(
53 innertext,
54 '<br>',
55 Link(self.GetScriptURL('admin'),
56 _('%(realname)s administrative interface')),
57 _(' (requires authorization)'),
58 '<br>',
59 Link(Utils.ScriptURL('listinfo'),
60 _('Overview of all %(hostname)s mailing lists')),
61 '<p>', MailmanLogo()))).Format()
63 def FormatUsers(self, digest, lang=None):
64 if lang is None:
65 lang = self.preferred_language
66 conceal_sub = Defaults.ConcealSubscription
67 people = []
68 if digest:
69 digestmembers = self.getDigestMemberKeys()
70 for dm in digestmembers:
71 if not self.getMemberOption(dm, conceal_sub):
72 people.append(dm)
73 num_concealed = len(digestmembers) - len(people)
74 else:
75 members = self.getRegularMemberKeys()
76 for m in members:
77 if not self.getMemberOption(m, conceal_sub):
78 people.append(m)
79 num_concealed = len(members) - len(people)
80 if num_concealed == 1:
81 concealed = _('<em>(1 private member not shown)</em>')
82 elif num_concealed > 1:
83 concealed = _(
84 '<em>(%(num_concealed)d private members not shown)</em>')
85 else:
86 concealed = ''
87 items = []
88 people.sort()
89 obscure = self.obscure_addresses
90 for person in people:
91 id = Utils.ObscureEmail(person)
92 url = self.GetOptionsURL(person, obscure=obscure)
93 if obscure:
94 showing = Utils.ObscureEmail(person, for_text=1)
95 else:
96 showing = person
97 got = Link(url, showing)
98 if self.getDeliveryStatus(person) <> MemberAdaptor.ENABLED:
99 got = Italic('(', got, ')')
100 items.append(got)
101 # Just return the .Format() so this works until I finish
102 # converting everything to htmlformat...
103 return concealed + UnorderedList(*tuple(items)).Format()
105 def FormatOptionButton(self, option, value, user):
106 if option == Defaults.DisableDelivery:
107 optval = self.getDeliveryStatus(user) <> MemberAdaptor.ENABLED
108 else:
109 optval = self.getMemberOption(user, option)
110 if optval == value:
111 checked = ' CHECKED'
112 else:
113 checked = ''
114 name = {
115 Defaults.DontReceiveOwnPosts : 'dontreceive',
116 Defaults.DisableDelivery : 'disablemail',
117 Defaults.DisableMime : 'mime',
118 Defaults.AcknowledgePosts : 'ackposts',
119 Defaults.Digests : 'digest',
120 Defaults.ConcealSubscription : 'conceal',
121 Defaults.SuppressPasswordReminder : 'remind',
122 Defaults.ReceiveNonmatchingTopics : 'rcvtopic',
123 Defaults.DontReceiveDuplicates : 'nodupes',
124 }[option]
125 return '<input type=radio name="%s" value="%d"%s>' % (
126 name, value, checked)
128 def FormatDigestButton(self):
129 if self.digest_is_default:
130 checked = ' CHECKED'
131 else:
132 checked = ''
133 return '<input type=radio name="digest" value="1"%s>' % checked
135 def FormatDisabledNotice(self, user):
136 status = self.getDeliveryStatus(user)
137 reason = None
138 info = self.getBounceInfo(user)
139 if status == MemberAdaptor.BYUSER:
140 reason = _('; it was disabled by you')
141 elif status == MemberAdaptor.BYADMIN:
142 reason = _('; it was disabled by the list administrator')
143 elif status == MemberAdaptor.BYBOUNCE:
144 date = time.strftime('%d-%b-%Y',
145 time.localtime(Utils.midnight(info.date)))
146 reason = _('''; it was disabled due to excessive bounces. The
147 last bounce was received on %(date)s''')
148 elif status == MemberAdaptor.UNKNOWN:
149 reason = _('; it was disabled for unknown reasons')
150 if reason:
151 note = FontSize('+1', _(
152 'Note: your list delivery is currently disabled%(reason)s.'
153 )).Format()
154 link = Link('#disable', _('Mail delivery')).Format()
155 mailto = Link('mailto:' + self.GetOwnerEmail(),
156 _('the list administrator')).Format()
157 return _('''<p>%(note)s
159 <p>You may have disabled list delivery intentionally,
160 or it may have been triggered by bounces from your email
161 address. In either case, to re-enable delivery, change the
162 %(link)s option below. Contact %(mailto)s if you have any
163 questions or need assistance.''')
164 elif info and info.score > 0:
165 # Provide information about their current bounce score. We know
166 # their membership is currently enabled.
167 score = info.score
168 total = self.bounce_score_threshold
169 return _('''<p>We have received some recent bounces from your
170 address. Your current <em>bounce score</em> is %(score)s out of a
171 maximum of %(total)s. Please double check that your subscribed
172 address is correct and that there are no problems with delivery to
173 this address. Your bounce score will be automatically reset if
174 the problems are corrected soon.''')
175 else:
176 return ''
178 def FormatUmbrellaNotice(self, user, type):
179 addr = self.GetMemberAdminEmail(user)
180 if self.umbrella_list:
181 return _("(Note - you are subscribing to a list of mailing lists, "
182 "so the %(type)s notice will be sent to the admin address"
183 " for your membership, %(addr)s.)<p>")
184 else:
185 return ""
187 def FormatSubscriptionMsg(self):
188 msg = ''
189 also = ''
190 if self.subscribe_policy == 1:
191 msg += _('''You will be sent email requesting confirmation, to
192 prevent others from gratuitously subscribing you.''')
193 elif self.subscribe_policy == 2:
194 msg += _("""This is a closed list, which means your subscription
195 will be held for approval. You will be notified of the list
196 moderator's decision by email.""")
197 also = _('also ')
198 elif self.subscribe_policy == 3:
199 msg += _("""You will be sent email requesting confirmation, to
200 prevent others from gratuitously subscribing you. Once
201 confirmation is received, your request will be held for approval
202 by the list moderator. You will be notified of the moderator's
203 decision by email.""")
204 also = _("also ")
205 if msg:
206 msg += ' '
207 if self.private_roster == 1:
208 msg += _('''This is %(also)sa private list, which means that the
209 list of members is not available to non-members.''')
210 elif self.private_roster:
211 msg += _('''This is %(also)sa hidden list, which means that the
212 list of members is available only to the list administrator.''')
213 else:
214 msg += _('''This is %(also)sa public list, which means that the
215 list of members list is available to everyone.''')
216 if self.obscure_addresses:
217 msg += _(''' (but we obscure the addresses so they are not
218 easily recognizable by spammers).''')
220 if self.umbrella_list:
221 sfx = self.umbrella_member_suffix
222 msg += _("""<p>(Note that this is an umbrella list, intended to
223 have only other mailing lists as members. Among other things,
224 this means that your confirmation request will be sent to the
225 `%(sfx)s' account for your address.)""")
226 return msg
228 def FormatUndigestButton(self):
229 if self.digest_is_default:
230 checked = ''
231 else:
232 checked = ' CHECKED'
233 return '<input type=radio name="digest" value="0"%s>' % checked
235 def FormatMimeDigestsButton(self):
236 if self.mime_is_default_digest:
237 checked = ' CHECKED'
238 else:
239 checked = ''
240 return '<input type=radio name="mime" value="1"%s>' % checked
242 def FormatPlainDigestsButton(self):
243 if self.mime_is_default_digest:
244 checked = ''
245 else:
246 checked = ' CHECKED'
247 return '<input type=radio name="plain" value="1"%s>' % checked
249 def FormatEditingOption(self, lang):
250 if self.private_roster == 0:
251 either = _('<b><i>either</i></b> ')
252 else:
253 either = ''
254 realname = self.real_name
256 text = (_('''To unsubscribe from %(realname)s, get a password reminder,
257 or change your subscription options %(either)senter your subscription
258 email address:
259 <p><center> ''')
260 + TextBox('email', size=30).Format()
261 + ' '
262 + SubmitButton('UserOptions',
263 _('Unsubscribe or edit options')).Format()
264 + Hidden('language', lang).Format()
265 + '</center>')
266 if self.private_roster == 0:
267 text += _('''<p>... <b><i>or</i></b> select your entry from
268 the subscribers list (see above).''')
269 text += _(''' If you leave the field blank, you will be prompted for
270 your email address''')
271 return text
273 def RestrictedListMessage(self, which, restriction):
274 if not restriction:
275 return ''
276 elif restriction == 1:
277 return _(
278 '''(<i>%(which)s is only available to the list
279 members.</i>)''')
280 else:
281 return _('''(<i>%(which)s is only available to the list
282 administrator.</i>)''')
284 def FormatRosterOptionForUser(self, lang):
285 return self.RosterOption(lang).Format()
287 def RosterOption(self, lang):
288 container = Container()
289 container.AddItem(Hidden('language', lang))
290 if not self.private_roster:
291 container.AddItem(_("Click here for the list of ")
292 + self.real_name
293 + _(" subscribers: "))
294 container.AddItem(SubmitButton('SubscriberRoster',
295 _("Visit Subscriber list")))
296 else:
297 if self.private_roster == 1:
298 only = _('members')
299 whom = _('Address:')
300 else:
301 only = _('the list administrator')
302 whom = _('Admin address:')
303 # Solicit the user and password.
304 container.AddItem(
305 self.RestrictedListMessage(_('The subscribers list'),
306 self.private_roster)
307 + _(" <p>Enter your ")
308 + whom[:-1].lower()
309 + _(" and password to visit"
310 " the subscribers list: <p><center> ")
311 + whom
312 + " ")
313 container.AddItem(self.FormatBox('roster-email'))
314 container.AddItem(_("Password: ")
315 + self.FormatSecureBox('roster-pw')
316 + "&nbsp;&nbsp;")
317 container.AddItem(SubmitButton('SubscriberRoster',
318 _('Visit Subscriber List')))
319 container.AddItem("</center>")
320 return container
322 def FormatFormStart(self, name, extra=''):
323 base_url = self.GetScriptURL(name)
324 if extra:
325 full_url = "%s/%s" % (base_url, extra)
326 else:
327 full_url = base_url
328 return ('<FORM Method=POST ACTION="%s">' % full_url)
330 def FormatArchiveAnchor(self):
331 return '<a href="%s">' % self.GetBaseArchiveURL()
333 def FormatFormEnd(self):
334 return '</FORM>'
336 def FormatBox(self, name, size=20, value=''):
337 return '<INPUT type="Text" name="%s" size="%d" value="%s">' % (
338 name, size, value)
340 def FormatSecureBox(self, name):
341 return '<INPUT type="Password" name="%s" size="15">' % name
343 def FormatButton(self, name, text='Submit'):
344 return '<INPUT type="Submit" name="%s" value="%s">' % (name, text)
346 def FormatReminder(self, lang):
347 if self.send_reminders:
348 return _('Once a month, your password will be emailed to you as'
349 ' a reminder.')
350 return ''
352 def ParseTags(self, template, replacements, lang=None):
353 if lang is None:
354 charset = 'us-ascii'
355 else:
356 charset = Utils.GetCharSet(lang)
357 text = Utils.maketext(template, raw=1, lang=lang, mlist=self)
358 parts = re.split('(</?[Mm][Mm]-[^>]*>)', text)
359 i = 1
360 while i < len(parts):
361 tag = parts[i].lower()
362 if replacements.has_key(tag):
363 repl = replacements[tag]
364 if isinstance(repl, str):
365 repl = unicode(repl, charset, 'replace')
366 parts[i] = repl
367 else:
368 parts[i] = ''
369 i = i + 2
370 return EMPTYSTRING.join(parts)
372 # This needs to wait until after the list is inited, so let's build it
373 # when it's needed only.
374 def GetStandardReplacements(self, lang=None):
375 dmember_len = len(self.getDigestMemberKeys())
376 member_len = len(self.getRegularMemberKeys())
377 # If only one language is enabled for this mailing list, omit the
378 # language choice buttons.
379 if len(self.language_codes) == 1:
380 listlangs = _(
381 config.languages.get_description(self.preferred_language))
382 else:
383 listlangs = self.GetLangSelectBox(lang).Format()
384 d = {
385 '<mm-mailman-footer>' : self.GetMailmanFooter(),
386 '<mm-list-name>' : self.real_name,
387 '<mm-email-user>' : self._internal_name,
388 '<mm-list-description>' : self.description,
389 '<mm-list-info>' : BR.join(self.info.split(NL)),
390 '<mm-form-end>' : self.FormatFormEnd(),
391 '<mm-archive>' : self.FormatArchiveAnchor(),
392 '</mm-archive>' : '</a>',
393 '<mm-list-subscription-msg>' : self.FormatSubscriptionMsg(),
394 '<mm-restricted-list-message>' : \
395 self.RestrictedListMessage(_('The current archive'),
396 self.archive_private),
397 '<mm-num-reg-users>' : `member_len`,
398 '<mm-num-digesters>' : `dmember_len`,
399 '<mm-num-members>' : (`member_len + dmember_len`),
400 '<mm-posting-addr>' : '%s' % self.GetListEmail(),
401 '<mm-request-addr>' : '%s' % self.GetRequestEmail(),
402 '<mm-owner>' : self.GetOwnerEmail(),
403 '<mm-reminder>' : self.FormatReminder(self.preferred_language),
404 '<mm-host>' : self.host_name,
405 '<mm-list-langs>' : listlangs,
407 if config.IMAGE_LOGOS:
408 d['<mm-favicon>'] = config.IMAGE_LOGOS + config.SHORTCUT_ICON
409 return d
411 def GetAllReplacements(self, lang=None):
413 returns standard replaces plus formatted user lists in
414 a dict just like GetStandardReplacements.
416 if lang is None:
417 lang = self.preferred_language
418 d = self.GetStandardReplacements(lang)
419 d.update({"<mm-regular-users>": self.FormatUsers(0, lang),
420 "<mm-digest-users>": self.FormatUsers(1, lang)})
421 return d
423 def GetLangSelectBox(self, lang=None, varname='language'):
424 if lang is None:
425 lang = self.preferred_language
426 # Figure out the available languages
427 values = self.language_codes
428 legend = [config.languages.get_description(code) for code in values]
429 try:
430 selected = values.index(lang)
431 except ValueError:
432 try:
433 selected = values.index(self.preferred_language)
434 except ValueError:
435 selected = config.DEFAULT_SERVER_LANGUAGE
436 # Return the widget
437 return SelectOptions(varname, values, legend, selected)