OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.mail.test.test_mail -*- | |
2 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 """Mail support for twisted python. | |
7 """ | |
8 | |
9 # Twisted imports | |
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 | |
14 | |
15 from twisted import cred | |
16 import twisted.cred.portal | |
17 | |
18 # Sibling imports | |
19 from twisted.mail import protocols, smtp | |
20 | |
21 # System imports | |
22 import os | |
23 from zope.interface import implements, Interface | |
24 | |
25 | |
26 class DomainWithDefaultDict: | |
27 '''Simulate a dictionary with a default value for non-existing keys. | |
28 ''' | |
29 def __init__(self, domains, default): | |
30 self.domains = domains | |
31 self.default = default | |
32 | |
33 def setDefaultDomain(self, domain): | |
34 self.default = domain | |
35 | |
36 def has_key(self, name): | |
37 return 1 | |
38 | |
39 def fromkeys(klass, keys, value=None): | |
40 d = klass() | |
41 for k in keys: | |
42 d[k] = value | |
43 return d | |
44 fromkeys = classmethod(fromkeys) | |
45 | |
46 def __contains__(self, name): | |
47 return 1 | |
48 | |
49 def __getitem__(self, name): | |
50 return self.domains.get(name, self.default) | |
51 | |
52 def __setitem__(self, name, value): | |
53 self.domains[name] = value | |
54 | |
55 def __delitem__(self, name): | |
56 del self.domains[name] | |
57 | |
58 def __iter__(self): | |
59 return iter(self.domains) | |
60 | |
61 def __len__(self): | |
62 return len(self.domains) | |
63 | |
64 | |
65 def __str__(self): | |
66 """ | |
67 Return a string describing the underlying domain mapping of this | |
68 object. | |
69 """ | |
70 return '<DomainWithDefaultDict %s>' % (self.domains,) | |
71 | |
72 | |
73 def __repr__(self): | |
74 """ | |
75 Return a pseudo-executable string describing the underlying domain | |
76 mapping of this object. | |
77 """ | |
78 return 'DomainWithDefaultDict(%s)' % (self.domains,) | |
79 | |
80 | |
81 def get(self, key, default=None): | |
82 return self.domains.get(key, default) | |
83 | |
84 def copy(self): | |
85 return DomainWithDefaultDict(self.domains.copy(), self.default) | |
86 | |
87 def iteritems(self): | |
88 return self.domains.iteritems() | |
89 | |
90 def iterkeys(self): | |
91 return self.domains.iterkeys() | |
92 | |
93 def itervalues(self): | |
94 return self.domains.itervalues() | |
95 | |
96 def keys(self): | |
97 return self.domains.keys() | |
98 | |
99 def values(self): | |
100 return self.domains.values() | |
101 | |
102 def items(self): | |
103 return self.domains.items() | |
104 | |
105 def popitem(self): | |
106 return self.domains.popitem() | |
107 | |
108 def update(self, other): | |
109 return self.domains.update(other) | |
110 | |
111 def clear(self): | |
112 return self.domains.clear() | |
113 | |
114 def setdefault(self, key, default): | |
115 return self.domains.setdefault(key, default) | |
116 | |
117 class IDomain(Interface): | |
118 """An email domain.""" | |
119 | |
120 def exists(user): | |
121 """ | |
122 Check whether or not the specified user exists in this domain. | |
123 | |
124 @type user: C{twisted.protocols.smtp.User} | |
125 @param user: The user to check | |
126 | |
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. | |
132 | |
133 @raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given | |
134 user does not exist in this domain. | |
135 """ | |
136 | |
137 def addUser(user, password): | |
138 """Add a username/password to this domain.""" | |
139 | |
140 def startMessage(user): | |
141 """Create and return a new message to be delivered to the given user. | |
142 | |
143 DEPRECATED. Implement validateTo() correctly instead. | |
144 """ | |
145 | |
146 def getCredentialsCheckers(): | |
147 """Return a list of ICredentialsChecker implementors for this domain. | |
148 """ | |
149 | |
150 class IAliasableDomain(IDomain): | |
151 def setAliasGroup(aliases): | |
152 """Set the group of defined aliases for this domain | |
153 | |
154 @type aliases: C{dict} | |
155 @param aliases: Mapping of domain names to objects implementing | |
156 C{IAlias} | |
157 """ | |
158 | |
159 def exists(user, memo=None): | |
160 """ | |
161 Check whether or not the specified user exists in this domain. | |
162 | |
163 @type user: C{twisted.protocols.smtp.User} | |
164 @param user: The user to check | |
165 | |
166 @type memo: C{dict} | |
167 @param memo: A record of the addresses already considered while | |
168 resolving aliases. The default value should be used by all | |
169 external code. | |
170 | |
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. | |
176 | |
177 @raise twisted.protocols.smtp.SMTPBadRcpt: Raised if the given | |
178 user does not exist in this domain. | |
179 """ | |
180 | |
181 class BounceDomain: | |
182 """A domain in which no user exists. | |
183 | |
184 This can be used to block off certain domains. | |
185 """ | |
186 | |
187 implements(IDomain) | |
188 | |
189 def exists(self, user): | |
190 raise smtp.SMTPBadRcpt(user) | |
191 | |
192 def willRelay(self, user, protocol): | |
193 return False | |
194 | |
195 def addUser(self, user, password): | |
196 pass | |
197 | |
198 def startMessage(self, user): | |
199 """ | |
200 No code should ever call this function. | |
201 """ | |
202 raise NotImplementedError( | |
203 "No code should ever call this method for any reason") | |
204 | |
205 def getCredentialsCheckers(self): | |
206 return [] | |
207 | |
208 | |
209 class FileMessage: | |
210 """A file we can write an email too.""" | |
211 | |
212 implements(smtp.IMessage) | |
213 | |
214 def __init__(self, fp, name, finalName): | |
215 self.fp = fp | |
216 self.name = name | |
217 self.finalName = finalName | |
218 | |
219 def lineReceived(self, line): | |
220 self.fp.write(line+'\n') | |
221 | |
222 def eomReceived(self): | |
223 self.fp.close() | |
224 os.rename(self.name, self.finalName) | |
225 return defer.succeed(self.finalName) | |
226 | |
227 def connectionLost(self): | |
228 self.fp.close() | |
229 os.remove(self.name) | |
230 | |
231 | |
232 class MailService(service.MultiService): | |
233 """An email service.""" | |
234 | |
235 queue = None | |
236 domains = None | |
237 portals = None | |
238 aliases = None | |
239 smtpPortal = None | |
240 | |
241 def __init__(self): | |
242 service.MultiService.__init__(self) | |
243 # Domains and portals for "client" protocols - POP3, IMAP4, etc | |
244 self.domains = DomainWithDefaultDict({}, BounceDomain()) | |
245 self.portals = {} | |
246 | |
247 self.monitor = FileMonitoringService() | |
248 self.monitor.setServiceParent(self) | |
249 self.smtpPortal = cred.portal.Portal(self) | |
250 | |
251 def getPOP3Factory(self): | |
252 return protocols.POP3Factory(self) | |
253 | |
254 def getSMTPFactory(self): | |
255 return protocols.SMTPFactory(self, self.smtpPortal) | |
256 | |
257 def getESMTPFactory(self): | |
258 return protocols.ESMTPFactory(self, self.smtpPortal) | |
259 | |
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) | |
267 | |
268 def setQueue(self, queue): | |
269 """Set the queue for outgoing emails.""" | |
270 self.queue = queue | |
271 | |
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() | |
277 | |
278 def lookupPortal(self, name): | |
279 return self.portals[name] | |
280 | |
281 def defaultPortal(self): | |
282 return self.portals[''] | |
283 | |
284 | |
285 class FileMonitoringService(internet.TimerService): | |
286 | |
287 def __init__(self): | |
288 self.files = [] | |
289 self.intervals = iter(util.IntervalDifferential([], 60)) | |
290 | |
291 def startService(self): | |
292 service.Service.startService(self) | |
293 self._setupMonitor() | |
294 | |
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) | |
299 | |
300 def stopService(self): | |
301 service.Service.stopService(self) | |
302 if self._call: | |
303 self._call.cancel() | |
304 self._call = None | |
305 | |
306 def monitorFile(self, name, callback, interval=10): | |
307 try: | |
308 mtime = os.path.getmtime(name) | |
309 except: | |
310 mtime = 0 | |
311 self.files.append([interval, name, callback, mtime]) | |
312 self.intervals.addInterval(interval) | |
313 | |
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]) | |
318 del self.files[i] | |
319 break | |
320 | |
321 def _monitor(self): | |
322 self._call = None | |
323 if self.index is not None: | |
324 name, callback, mtime = self.files[self.index][1:] | |
325 try: | |
326 now = os.path.getmtime(name) | |
327 except: | |
328 now = 0 | |
329 if now > mtime: | |
330 log.msg("%s changed, notifying listener" % (name,)) | |
331 self.files[self.index][3] = now | |
332 callback(name) | |
333 self._setupMonitor() | |
OLD | NEW |