1
# -*- test-case-name: twisted.mail.test.test_mail -*-
2
# Copyright (c) 2001-2007 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)
67
Return a string describing the underlying domain mapping of this
70
return '<DomainWithDefaultDict %s>' % (self.domains,)
75
Return a pseudo-executable string describing the underlying domain
76
mapping of this object.
78
return 'DomainWithDefaultDict(%s)' % (self.domains,)
81
def get(self, key, default=None):
82
return self.domains.get(key, default)
85
return DomainWithDefaultDict(self.domains.copy(), self.default)
88
return self.domains.iteritems()
91
return self.domains.iterkeys()
94
return self.domains.itervalues()
97
return self.domains.keys()
100
return self.domains.values()
103
return self.domains.items()
106
return self.domains.popitem()
108
def update(self, other):
109
return self.domains.update(other)
112
return self.domains.clear()
114
def setdefault(self, key, default):
115
return self.domains.setdefault(key, default)
117
class IDomain(Interface):
118
"""An email domain."""
122
Check whether or not the specified user exists in this domain.
124
@type user: C{twisted.protocols.smtp.User}
125
@param user: The user to check
127
@rtype: No-argument callable
128
@return: A C{Deferred} which becomes, or a callable which
129
takes no arguments and returns an object implementing C{IMessage}.
130
This will be called and the returned object used to deliver the
131
message when it arrives.
133
@raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given
134
user does not exist in this domain.
137
def addUser(user, password):
138
"""Add a username/password to this domain."""
140
def startMessage(user):
141
"""Create and return a new message to be delivered to the given user.
143
DEPRECATED. Implement validateTo() correctly instead.
146
def getCredentialsCheckers():
147
"""Return a list of ICredentialsChecker implementors for this domain.
150
class IAliasableDomain(IDomain):
151
def setAliasGroup(aliases):
152
"""Set the group of defined aliases for this domain
154
@type aliases: C{dict}
155
@param aliases: Mapping of domain names to objects implementing
159
def exists(user, memo=None):
161
Check whether or not the specified user exists in this domain.
163
@type user: C{twisted.protocols.smtp.User}
164
@param user: The user to check
167
@param memo: A record of the addresses already considered while
168
resolving aliases. The default value should be used by all
171
@rtype: No-argument callable
172
@return: A C{Deferred} which becomes, or a callable which
173
takes no arguments and returns an object implementing C{IMessage}.
174
This will be called and the returned object used to deliver the
175
message when it arrives.
177
@raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given
178
user does not exist in this domain.
182
"""A domain in which no user exists.
184
This can be used to block off certain domains.
189
def exists(self, user):
190
raise smtp.SMTPBadRcpt(user)
192
def willRelay(self, user, protocol):
195
def addUser(self, user, password):
198
def startMessage(self, user):
200
No code should ever call this function.
202
raise NotImplementedError(
203
"No code should ever call this method for any reason")
205
def getCredentialsCheckers(self):
210
"""A file we can write an email too."""
212
implements(smtp.IMessage)
214
def __init__(self, fp, name, finalName):
217
self.finalName = finalName
219
def lineReceived(self, line):
220
self.fp.write(line+'\n')
222
def eomReceived(self):
224
os.rename(self.name, self.finalName)
225
return defer.succeed(self.finalName)
227
def connectionLost(self):
232
class MailService(service.MultiService):
233
"""An email service."""
242
service.MultiService.__init__(self)
243
# Domains and portals for "client" protocols - POP3, IMAP4, etc
244
self.domains = DomainWithDefaultDict({}, BounceDomain())
247
self.monitor = FileMonitoringService()
248
self.monitor.setServiceParent(self)
249
self.smtpPortal = cred.portal.Portal(self)
251
def getPOP3Factory(self):
252
return protocols.POP3Factory(self)
254
def getSMTPFactory(self):
255
return protocols.SMTPFactory(self, self.smtpPortal)
257
def getESMTPFactory(self):
258
return protocols.ESMTPFactory(self, self.smtpPortal)
260
def addDomain(self, name, domain):
261
portal = cred.portal.Portal(domain)
262
map(portal.registerChecker, domain.getCredentialsCheckers())
263
self.domains[name] = domain
264
self.portals[name] = portal
265
if self.aliases and IAliasableDomain.providedBy(domain):
266
domain.setAliasGroup(self.aliases)
268
def setQueue(self, queue):
269
"""Set the queue for outgoing emails."""
272
def requestAvatar(self, avatarId, mind, *interfaces):
273
if smtp.IMessageDelivery in interfaces:
274
a = protocols.ESMTPDomainDelivery(self, avatarId)
275
return smtp.IMessageDelivery, a, lambda: None
276
raise NotImplementedError()
278
def lookupPortal(self, name):
279
return self.portals[name]
281
def defaultPortal(self):
282
return self.portals['']
285
class FileMonitoringService(internet.TimerService):
289
self.intervals = iter(util.IntervalDifferential([], 60))
291
def startService(self):
292
service.Service.startService(self)
295
def _setupMonitor(self):
296
from twisted.internet import reactor
297
t, self.index = self.intervals.next()
298
self._call = reactor.callLater(t, self._monitor)
300
def stopService(self):
301
service.Service.stopService(self)
306
def monitorFile(self, name, callback, interval=10):
308
mtime = os.path.getmtime(name)
311
self.files.append([interval, name, callback, mtime])
312
self.intervals.addInterval(interval)
314
def unmonitorFile(self, name):
315
for i in range(len(self.files)):
316
if name == self.files[i][1]:
317
self.intervals.removeInterval(self.files[i][0])
323
if self.index is not None:
324
name, callback, mtime = self.files[self.index][1:]
326
now = os.path.getmtime(name)
330
log.msg("%s changed, notifying listener" % (name,))
331
self.files[self.index][3] = now