1
# -*- test-case-name: twisted.mail.test.test_mail -*-
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
"""Mail support for twisted python.
10
from twisted.internet import defer
11
from twisted.application import service, internet
12
from twisted.python import util
13
from twisted.python import log
15
from twisted import cred
16
import twisted.cred.portal
19
from twisted.mail import protocols, smtp
23
from zope.interface import implements, Interface
26
class DomainWithDefaultDict:
27
'''Simulate a dictionary with a default value for non-existing keys.
29
def __init__(self, domains, default):
30
self.domains = domains
31
self.default = default
33
def setDefaultDomain(self, domain):
36
def has_key(self, name):
39
def fromkeys(klass, keys, value=None):
44
fromkeys = classmethod(fromkeys)
46
def __contains__(self, name):
49
def __getitem__(self, name):
50
return self.domains.get(name, self.default)
52
def __setitem__(self, name, value):
53
self.domains[name] = value
55
def __delitem__(self, name):
56
del self.domains[name]
59
return iter(self.domains)
62
return len(self.domains)
65
return '<DomainWithDefaultsDict %s>' % (self.domains,)
68
return 'DomainWithDefaultsDict(%s)>' % (self.domains,)
70
def get(self, key, default=None):
71
return self.domains.get(key, default)
74
return DomainWithDefaultsDict(self.domains.copy(), self.default)
77
return self.domains.iteritems()
80
return self.domains.iterkeys()
83
return self.domains.itervalues()
86
return self.domains.keys()
89
return self.domains.values()
92
return self.domains.items()
95
return self.domains.popitem()
97
def update(self, other):
98
return self.domains.update(other)
101
return self.domains.clear()
103
def setdefault(self, key, default):
104
return self.domains.setdefault(key, default)
106
class IDomain(Interface):
107
"""An email domain."""
111
Check whether or not the specified user exists in this domain.
113
@type user: C{twisted.protocols.smtp.User}
114
@param user: The user to check
116
@rtype: No-argument callable
117
@return: A C{Deferred} which becomes, or a callable which
118
takes no arguments and returns an object implementing C{IMessage}.
119
This will be called and the returned object used to deliver the
120
message when it arrives.
122
@raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given
123
user does not exist in this domain.
126
def addUser(user, password):
127
"""Add a username/password to this domain."""
129
def startMessage(user):
130
"""Create and return a new message to be delivered to the given user.
132
DEPRECATED. Implement validateTo() correctly instead.
135
def getCredentialsCheckers():
136
"""Return a list of ICredentialsChecker implementors for this domain.
139
class IAliasableDomain(IDomain):
140
def setAliasGroup(aliases):
141
"""Set the group of defined aliases for this domain
143
@type aliases: C{dict}
144
@param aliases: Mapping of domain names to objects implementing
148
def exists(user, memo=None):
150
Check whether or not the specified user exists in this domain.
152
@type user: C{twisted.protocols.smtp.User}
153
@param user: The user to check
156
@param memo: A record of the addresses already considered while
157
resolving aliases. The default value should be used by all
160
@rtype: No-argument callable
161
@return: A C{Deferred} which becomes, or a callable which
162
takes no arguments and returns an object implementing C{IMessage}.
163
This will be called and the returned object used to deliver the
164
message when it arrives.
166
@raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given
167
user does not exist in this domain.
171
"""A domain in which no user exists.
173
This can be used to block off certain domains.
178
def exists(self, user):
179
raise smtp.SMTPBadRcpt(user)
181
def willRelay(self, user, protocol):
184
def addUser(self, user, password):
187
def startMessage(self, user):
188
raise AssertionError, "No code should ever call this method for any reason"
190
def getCredentialsCheckers(self):
195
"""A file we can write an email too."""
197
implements(smtp.IMessage)
199
def __init__(self, fp, name, finalName):
202
self.finalName = finalName
204
def lineReceived(self, line):
205
self.fp.write(line+'\n')
207
def eomReceived(self):
209
os.rename(self.name, self.finalName)
210
return defer.succeed(self.finalName)
212
def connectionLost(self):
217
class MailService(service.MultiService):
218
"""An email service."""
227
service.MultiService.__init__(self)
228
# Domains and portals for "client" protocols - POP3, IMAP4, etc
229
self.domains = DomainWithDefaultDict({}, BounceDomain())
232
self.monitor = FileMonitoringService()
233
self.monitor.setServiceParent(self)
234
self.smtpPortal = cred.portal.Portal(self)
236
def getPOP3Factory(self):
237
return protocols.POP3Factory(self)
239
def getSMTPFactory(self):
240
return protocols.SMTPFactory(self, self.smtpPortal)
242
def getESMTPFactory(self):
243
return protocols.ESMTPFactory(self, self.smtpPortal)
245
def addDomain(self, name, domain):
246
portal = cred.portal.Portal(domain)
247
map(portal.registerChecker, domain.getCredentialsCheckers())
248
self.domains[name] = domain
249
self.portals[name] = portal
250
if self.aliases and IAliasableDomain.providedBy(domain):
251
domain.setAliasGroup(self.aliases)
253
def setQueue(self, queue):
254
"""Set the queue for outgoing emails."""
257
def requestAvatar(self, avatarId, mind, *interfaces):
258
if smtp.IMessageDelivery in interfaces:
259
a = protocols.ESMTPDomainDelivery(self, avatarId)
260
return smtp.IMessageDelivery, a, lambda: None
261
raise NotImplementedError()
263
def lookupPortal(self, name):
264
return self.portals[name]
266
def defaultPortal(self):
267
return self.portals['']
270
class FileMonitoringService(internet.TimerService):
274
self.intervals = iter(util.IntervalDifferential([], 60))
276
def startService(self):
277
service.Service.startService(self)
280
def _setupMonitor(self):
281
from twisted.internet import reactor
282
t, self.index = self.intervals.next()
283
self._call = reactor.callLater(t, self._monitor)
285
def stopService(self):
286
service.Service.stopService(self)
291
def monitorFile(self, name, callback, interval=10):
293
mtime = os.path.getmtime(name)
296
self.files.append([interval, name, callback, mtime])
297
self.intervals.addInterval(interval)
299
def unmonitorFile(self, name):
300
for i in range(len(self.files)):
301
if name == self.files[i][1]:
302
self.intervals.removeInterval(self.files[i][0])
308
if self.index is not None:
309
name, callback, mtime = self.files[self.index][1:]
311
now = os.path.getmtime(name)
315
log.msg("%s changed, notifying listener" % (name,))
316
self.files[self.index][3] = now