1 # Copyright (C) 2006-2012 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Model for mailing lists."""
20 from __future__
import absolute_import
, print_function
, unicode_literals
30 from storm
.locals import (
31 And
, Bool
, DateTime
, Float
, Int
, Pickle
, RawStr
, Reference
, Store
,
33 from urlparse
import urljoin
34 from zope
.component
import getUtility
35 from zope
.event
import notify
36 from zope
.interface
import implementer
38 from mailman
.config
import config
39 from mailman
.database
.model
import Model
40 from mailman
.database
.types
import Enum
41 from mailman
.interfaces
.action
import Action
, FilterAction
42 from mailman
.interfaces
.address
import IAddress
43 from mailman
.interfaces
.archiver
import ArchivePolicy
44 from mailman
.interfaces
.autorespond
import ResponseAction
45 from mailman
.interfaces
.bounce
import UnrecognizedBounceDisposition
46 from mailman
.interfaces
.digests
import DigestFrequency
47 from mailman
.interfaces
.domain
import IDomainManager
48 from mailman
.interfaces
.languages
import ILanguageManager
49 from mailman
.interfaces
.mailinglist
import (
50 IAcceptableAlias
, IAcceptableAliasSet
, IMailingList
, Personalization
,
52 from mailman
.interfaces
.member
import (
53 AlreadySubscribedError
, MemberRole
, MissingPreferredAddressError
,
55 from mailman
.interfaces
.mime
import FilterType
56 from mailman
.interfaces
.nntp
import NewsgroupModeration
57 from mailman
.interfaces
.user
import IUser
58 from mailman
.model
import roster
59 from mailman
.model
.digests
import OneLastDigest
60 from mailman
.model
.member
import Member
61 from mailman
.model
.mime
import ContentFilter
62 from mailman
.model
.preferences
import Preferences
63 from mailman
.utilities
.filesystem
import makedirs
64 from mailman
.utilities
.string
import expand
72 @implementer(IMailingList
)
73 class MailingList(Model
):
74 """See `IMailingList`."""
76 id = Int(primary
=True)
78 # XXX denotes attributes that should be part of the public interface but
79 # are currently missing.
84 _list_id
= Unicode(name
='list_id')
85 allow_list_posts
= Bool()
86 include_rfc2369_headers
= Bool()
88 anonymous_list
= Bool()
89 # Attributes not directly modifiable via the web u/i
90 created_at
= DateTime()
91 admin_member_chunksize
= Int()
92 # Attributes which are directly modifiable via the web u/i. The more
93 # complicated attributes are currently stored as pickles, though that
94 # will change as the schema and implementation is developed.
95 next_request_id
= Int()
96 next_digest_number
= Int()
97 digest_last_sent_at
= DateTime()
99 last_post_at
= DateTime()
100 # Implicit destination.
101 acceptable_aliases_id
= Int()
102 acceptable_alias
= Reference(acceptable_aliases_id
, 'AcceptableAlias.id')
103 # Attributes which are directly modifiable via the web u/i. The more
104 # complicated attributes are currently stored as pickles, though that
105 # will change as the schema and implementation is developed.
106 accept_these_nonmembers
= Pickle() # XXX
107 admin_immed_notify
= Bool()
108 admin_notify_mchanges
= Bool()
109 administrivia
= Bool()
110 archive_policy
= Enum(ArchivePolicy
)
111 # Automatic responses.
112 autoresponse_grace_period
= TimeDelta()
113 autorespond_owner
= Enum(ResponseAction
)
114 autoresponse_owner_text
= Unicode()
115 autorespond_postings
= Enum(ResponseAction
)
116 autoresponse_postings_text
= Unicode()
117 autorespond_requests
= Enum(ResponseAction
)
118 autoresponse_request_text
= Unicode()
120 filter_action
= Enum(FilterAction
)
121 filter_content
= Bool()
122 collapse_alternatives
= Bool()
123 convert_html_to_plaintext
= Bool()
125 bounce_info_stale_after
= TimeDelta() # XXX
126 bounce_matching_headers
= Unicode() # XXX
127 bounce_notify_owner_on_disable
= Bool() # XXX
128 bounce_notify_owner_on_removal
= Bool() # XXX
129 bounce_score_threshold
= Int() # XXX
130 bounce_you_are_disabled_warnings
= Int() # XXX
131 bounce_you_are_disabled_warnings_interval
= TimeDelta() # XXX
132 forward_unrecognized_bounces_to
= Enum(UnrecognizedBounceDisposition
)
133 process_bounces
= Bool()
135 default_member_action
= Enum(Action
)
136 default_nonmember_action
= Enum(Action
)
137 description
= Unicode()
138 digest_footer_uri
= Unicode()
139 digest_header_uri
= Unicode()
140 digest_is_default
= Bool()
141 digest_send_periodic
= Bool()
142 digest_size_threshold
= Float()
143 digest_volume_frequency
= Enum(DigestFrequency
)
145 discard_these_nonmembers
= Pickle()
147 encode_ascii_prefixes
= Bool()
148 first_strip_reply_to
= Bool()
149 footer_uri
= Unicode()
150 forward_auto_discards
= Bool()
151 gateway_to_mail
= Bool()
152 gateway_to_news
= Bool()
153 goodbye_message_uri
= Unicode()
154 header_matches
= Pickle()
155 header_uri
= Unicode()
156 hold_these_nonmembers
= Pickle()
158 linked_newsgroup
= Unicode()
159 max_days_to_hold
= Int()
160 max_message_size
= Int()
161 max_num_recipients
= Int()
162 member_moderation_notice
= Unicode()
163 mime_is_default_digest
= Bool()
164 moderator_password
= RawStr()
165 newsgroup_moderation
= Enum(NewsgroupModeration
)
166 nntp_prefix_subject_too
= Bool()
167 nondigestable
= Bool()
168 nonmember_rejection_notice
= Unicode()
169 obscure_addresses
= Bool()
170 owner_chain
= Unicode()
171 owner_pipeline
= Unicode()
172 personalize
= Enum(Personalization
)
174 posting_chain
= Unicode()
175 posting_pipeline
= Unicode()
176 _preferred_language
= Unicode(name
='preferred_language')
177 private_roster
= Bool()
178 display_name
= Unicode()
179 reject_these_nonmembers
= Pickle()
180 reply_goes_to_list
= Enum(ReplyToMunging
)
181 reply_to_address
= Unicode()
182 require_explicit_destination
= Bool()
183 respond_to_post_requests
= Bool()
184 scrub_nondigest
= Bool()
185 send_goodbye_message
= Bool()
186 send_reminders
= Bool()
187 send_welcome_message
= Bool()
188 subject_prefix
= Unicode()
189 subscribe_auto_approval
= Pickle()
190 subscribe_policy
= Int()
192 topics_bodylines_limit
= Int()
193 topics_enabled
= Bool()
194 unsubscribe_policy
= Int()
195 welcome_message_uri
= Unicode()
197 def __init__(self
, fqdn_listname
):
198 super(MailingList
, self
).__init
__()
199 listname
, at
, hostname
= fqdn_listname
.partition('@')
200 assert hostname
, 'Bad list name: {0}'.format(fqdn_listname
)
201 self
.list_name
= listname
202 self
.mail_host
= hostname
203 self
._list
_id
= '{0}.{1}'.format(listname
, hostname
)
204 # For the pending database
205 self
.next_request_id
= 1
206 # We need to set up the rosters. Normally, this method will get
207 # called when the MailingList object is loaded from the database, but
208 # that's not the case when the constructor is called. So, set up the
209 # rosters explicitly.
210 self
.__storm
_loaded
__()
211 makedirs(self
.data_path
)
213 def __storm_loaded__(self
):
214 self
.owners
= roster
.OwnerRoster(self
)
215 self
.moderators
= roster
.ModeratorRoster(self
)
216 self
.administrators
= roster
.AdministratorRoster(self
)
217 self
.members
= roster
.MemberRoster(self
)
218 self
.regular_members
= roster
.RegularMemberRoster(self
)
219 self
.digest_members
= roster
.DigestMemberRoster(self
)
220 self
.subscribers
= roster
.Subscribers(self
)
221 self
.nonmembers
= roster
.NonmemberRoster(self
)
224 return '<mailing list "{0}" at {1:#x}>'.format(
225 self
.fqdn_listname
, id(self
))
228 def fqdn_listname(self
):
229 """See `IMailingList`."""
230 return '{0}@{1}'.format(self
.list_name
, self
.mail_host
)
234 """See `IMailingList`."""
239 """See `IMailingList`."""
240 return getUtility(IDomainManager
)[self
.mail_host
]
244 """See `IMailingList`."""
245 return self
.domain
.scheme
249 """See `IMailingList`."""
250 return self
.domain
.url_host
252 def script_url(self
, target
, context
=None):
253 """See `IMailingList`."""
254 # XXX Handle the case for when context is not None; those would be
256 return urljoin(self
.domain
.base_url
, target
+ '/' + self
.fqdn_listname
)
260 """See `IMailingList`."""
261 return os
.path
.join(config
.LIST_DATA_DIR
, self
.fqdn_listname
)
263 # IMailingListAddresses
266 def posting_address(self
):
267 """See `IMailingList`."""
268 return self
.fqdn_listname
271 def no_reply_address(self
):
272 """See `IMailingList`."""
273 return '{0}@{1}'.format(config
.mailman
.noreply_address
, self
.mail_host
)
276 def owner_address(self
):
277 """See `IMailingList`."""
278 return '{0}-owner@{1}'.format(self
.list_name
, self
.mail_host
)
281 def request_address(self
):
282 """See `IMailingList`."""
283 return '{0}-request@{1}'.format(self
.list_name
, self
.mail_host
)
286 def bounces_address(self
):
287 """See `IMailingList`."""
288 return '{0}-bounces@{1}'.format(self
.list_name
, self
.mail_host
)
291 def join_address(self
):
292 """See `IMailingList`."""
293 return '{0}-join@{1}'.format(self
.list_name
, self
.mail_host
)
296 def leave_address(self
):
297 """See `IMailingList`."""
298 return '{0}-leave@{1}'.format(self
.list_name
, self
.mail_host
)
301 def subscribe_address(self
):
302 """See `IMailingList`."""
303 return '{0}-subscribe@{1}'.format(self
.list_name
, self
.mail_host
)
306 def unsubscribe_address(self
):
307 """See `IMailingList`."""
308 return '{0}-unsubscribe@{1}'.format(self
.list_name
, self
.mail_host
)
310 def confirm_address(self
, cookie
):
311 """See `IMailingList`."""
312 local_part
= expand(config
.mta
.verp_confirm_format
, dict(
313 address
= '{0}-confirm'.format(self
.list_name
),
315 return '{0}@{1}'.format(local_part
, self
.mail_host
)
318 def preferred_language(self
):
319 """See `IMailingList`."""
320 return getUtility(ILanguageManager
)[self
._preferred
_language
]
322 @preferred_language.setter
323 def preferred_language(self
, language
):
324 """See `IMailingList`."""
325 # Accept both a language code and a `Language` instance.
327 self
._preferred
_language
= language
.code
328 except AttributeError:
329 self
._preferred
_language
= language
331 def send_one_last_digest_to(self
, address
, delivery_mode
):
332 """See `IMailingList`."""
333 digest
= OneLastDigest(self
, address
, delivery_mode
)
334 Store
.of(self
).add(digest
)
337 def last_digest_recipients(self
):
338 """See `IMailingList`."""
339 results
= Store
.of(self
).find(
341 OneLastDigest
.mailing_list
== self
)
342 recipients
= [(digest
.address
, digest
.delivery_mode
)
343 for digest
in results
]
348 def filter_types(self
):
349 """See `IMailingList`."""
350 results
= Store
.of(self
).find(
352 And(ContentFilter
.mailing_list
== self
,
353 ContentFilter
.filter_type
== FilterType
.filter_mime
))
354 for content_filter
in results
:
355 yield content_filter
.filter_pattern
358 def filter_types(self
, sequence
):
359 """See `IMailingList`."""
360 # First, delete all existing MIME type filter patterns.
361 store
= Store
.of(self
)
362 results
= store
.find(
364 And(ContentFilter
.mailing_list
== self
,
365 ContentFilter
.filter_type
== FilterType
.filter_mime
))
367 # Now add all the new filter types.
368 for mime_type
in sequence
:
369 content_filter
= ContentFilter(
370 self
, mime_type
, FilterType
.filter_mime
)
371 store
.add(content_filter
)
374 def pass_types(self
):
375 """See `IMailingList`."""
376 results
= Store
.of(self
).find(
378 And(ContentFilter
.mailing_list
== self
,
379 ContentFilter
.filter_type
== FilterType
.pass_mime
))
380 for content_filter
in results
:
381 yield content_filter
.filter_pattern
384 def pass_types(self
, sequence
):
385 """See `IMailingList`."""
386 # First, delete all existing MIME type pass patterns.
387 store
= Store
.of(self
)
388 results
= store
.find(
390 And(ContentFilter
.mailing_list
== self
,
391 ContentFilter
.filter_type
== FilterType
.pass_mime
))
393 # Now add all the new filter types.
394 for mime_type
in sequence
:
395 content_filter
= ContentFilter(
396 self
, mime_type
, FilterType
.pass_mime
)
397 store
.add(content_filter
)
400 def filter_extensions(self
):
401 """See `IMailingList`."""
402 results
= Store
.of(self
).find(
404 And(ContentFilter
.mailing_list
== self
,
405 ContentFilter
.filter_type
== FilterType
.filter_extension
))
406 for content_filter
in results
:
407 yield content_filter
.filter_pattern
409 @filter_extensions.setter
410 def filter_extensions(self
, sequence
):
411 """See `IMailingList`."""
412 # First, delete all existing file extensions filter patterns.
413 store
= Store
.of(self
)
414 results
= store
.find(
416 And(ContentFilter
.mailing_list
== self
,
417 ContentFilter
.filter_type
== FilterType
.filter_extension
))
419 # Now add all the new filter types.
420 for mime_type
in sequence
:
421 content_filter
= ContentFilter(
422 self
, mime_type
, FilterType
.filter_extension
)
423 store
.add(content_filter
)
426 def pass_extensions(self
):
427 """See `IMailingList`."""
428 results
= Store
.of(self
).find(
430 And(ContentFilter
.mailing_list
== self
,
431 ContentFilter
.filter_type
== FilterType
.pass_extension
))
432 for content_filter
in results
:
433 yield content_filter
.pass_pattern
435 @pass_extensions.setter
436 def pass_extensions(self
, sequence
):
437 """See `IMailingList`."""
438 # First, delete all existing file extensions pass patterns.
439 store
= Store
.of(self
)
440 results
= store
.find(
442 And(ContentFilter
.mailing_list
== self
,
443 ContentFilter
.filter_type
== FilterType
.pass_extension
))
445 # Now add all the new filter types.
446 for mime_type
in sequence
:
447 content_filter
= ContentFilter(
448 self
, mime_type
, FilterType
.pass_extension
)
449 store
.add(content_filter
)
451 def get_roster(self
, role
):
452 """See `IMailingList`."""
453 if role
is MemberRole
.member
:
455 elif role
is MemberRole
.owner
:
457 elif role
is MemberRole
.moderator
:
458 return self
.moderators
461 'Undefined MemberRole: {0}'.format(role
))
463 def subscribe(self
, subscriber
, role
=MemberRole
.member
):
464 """See `IMailingList`."""
465 store
= Store
.of(self
)
466 if IAddress
.providedBy(subscriber
):
470 Member
.list_id
== self
._list
_id
,
471 Member
._address
== subscriber
).one()
473 raise AlreadySubscribedError(
474 self
.fqdn_listname
, subscriber
.email
, role
)
475 elif IUser
.providedBy(subscriber
):
476 if subscriber
.preferred_address
is None:
477 raise MissingPreferredAddressError(subscriber
)
481 Member
.list_id
== self
._list
_id
,
482 Member
._user
== subscriber
).one()
484 raise AlreadySubscribedError(
485 self
.fqdn_listname
, subscriber
, role
)
487 raise ValueError('subscriber must be an address or user')
488 member
= Member(role
=role
,
489 list_id
=self
._list
_id
,
490 subscriber
=subscriber
)
491 member
.preferences
= Preferences()
493 notify(SubscriptionEvent(self
, member
))
498 @implementer(IAcceptableAlias
)
499 class AcceptableAlias(Model
):
500 """See `IAcceptableAlias`."""
502 id = Int(primary
=True)
504 mailing_list_id
= Int()
505 mailing_list
= Reference(mailing_list_id
, MailingList
.id)
509 def __init__(self
, mailing_list
, alias
):
510 self
.mailing_list
= mailing_list
515 @implementer(IAcceptableAliasSet
)
516 class AcceptableAliasSet
:
517 """See `IAcceptableAliasSet`."""
519 def __init__(self
, mailing_list
):
520 self
._mailing
_list
= mailing_list
523 """See `IAcceptableAliasSet`."""
524 Store
.of(self
._mailing
_list
).find(
526 AcceptableAlias
.mailing_list
== self
._mailing
_list
).remove()
528 def add(self
, alias
):
529 if not (alias
.startswith('^') or '@' in alias
):
530 raise ValueError(alias
)
531 alias
= AcceptableAlias(self
._mailing
_list
, alias
.lower())
532 Store
.of(self
._mailing
_list
).add(alias
)
534 def remove(self
, alias
):
535 Store
.of(self
._mailing
_list
).find(
537 And(AcceptableAlias
.mailing_list
== self
._mailing
_list
,
538 AcceptableAlias
.alias
== alias
.lower())).remove()
542 aliases
= Store
.of(self
._mailing
_list
).find(
544 AcceptableAlias
.mailing_list
== self
._mailing
_list
)
545 for alias
in aliases
: