| 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 |