1 # -*- test-case-name: twisted.mail.test.test_mail -*-
2 # Copyright (c) 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