Advertisement
Guest User

Untitled

a guest
Oct 18th, 2018
189
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.61 KB | None | 0 0
  1. from twisted.mail import imap4, maildir
  2.  
  3. from twisted.internet import reactor, defer, protocol
  4.  
  5. from twisted.cred import portal, checkers, credentials
  6.  
  7. from twisted.cred import error as credError
  8.  
  9. from twisted.python import filepath
  10.  
  11. from zope.interface import implements
  12.  
  13. import time, os, random, pickle
  14.  
  15.  
  16.  
  17. MAILBOXDELIMITER = "."
  18.  
  19.  
  20.  
  21. class IMAPUserAccount(object):
  22.  
  23. implements(imap4.IAccount)
  24.  
  25.  
  26.  
  27. def __init__(self, userDir):
  28.  
  29. self.dir = userDir
  30.  
  31. self.mailboxCache = {}
  32.  
  33. # make sure Inbox exists
  34.  
  35. inbox = self._getMailbox("Inbox", create=True)
  36.  
  37.  
  38.  
  39. def listMailboxes(self, ref, wildcard):
  40.  
  41. for box in os.listdir(self.dir):
  42.  
  43. yield box, self._getMailbox(box)
  44.  
  45.  
  46.  
  47. def select(self, path, rw=True):
  48.  
  49. "return an object implementing IMailbox for the given path"
  50.  
  51. return self._getMailbox(path)
  52.  
  53.  
  54.  
  55. def _getMailbox(self, path, create=False):
  56.  
  57. """
  58.  
  59. Helper function to get a mailbox object at the given
  60.  
  61. path, optionally creating it if it doesn't already exist.
  62.  
  63. """
  64.  
  65. # According to the IMAP spec, Inbox is case-insensitive
  66.  
  67. pathParts = path.split(MAILBOXDELIMITER)
  68.  
  69. if pathParts[0].lower() == 'inbox': pathParts[0] = 'Inbox'
  70.  
  71. path = MAILBOXDELIMITER.join(pathParts)
  72.  
  73.  
  74.  
  75. if not self.mailboxCache.has_key(path):
  76.  
  77. fullPath = os.path.join(self.dir, path)
  78.  
  79. if not os.path.exists(fullPath):
  80.  
  81. if create:
  82.  
  83. maildir.initializeMaildir(fullPath)
  84.  
  85. else:
  86.  
  87. raise KeyError, "No such mailbox"
  88.  
  89. self.mailboxCache[path] = IMAPMailbox(fullPath)
  90.  
  91. return self.mailboxCache[path]
  92.  
  93.  
  94.  
  95. def create(self, path):
  96.  
  97. "create a mailbox at path and return it"
  98.  
  99. self._getMailbox(path, create=True)
  100.  
  101.  
  102.  
  103. def delete(self, path):
  104.  
  105. "delete the mailbox at path"
  106.  
  107. raise imap4.MailboxException("Permission denied.")
  108.  
  109.  
  110.  
  111. def rename(self, oldname, newname):
  112.  
  113. "rename a mailbox"
  114.  
  115. oldPath = os.path.join(self.dir, oldname)
  116.  
  117. newPath = os.path.join(self.dir, newname)
  118.  
  119. os.rename(oldPath, newPath)
  120.  
  121.  
  122.  
  123. def isSubscribed(self, path):
  124.  
  125. "return a true value if user is subscribed to the mailbox"
  126.  
  127. return self._getMailbox(path).metadata.get('subscribed', False)
  128.  
  129.  
  130.  
  131. def subscribe(self, path):
  132.  
  133. "mark a mailbox as subscribed"
  134.  
  135. box = self._getMailbox(path)
  136.  
  137. box.metadata['subscribed'] = True
  138.  
  139. box.saveMetadata()
  140.  
  141. return True
  142.  
  143.  
  144.  
  145. def unsubscribe(self, path):
  146.  
  147. "mark a mailbox as unsubscribed"
  148.  
  149. box = self._getMailbox(path)
  150.  
  151. box.metadata['subscribed'] = False
  152.  
  153. box.saveMetadata()
  154.  
  155. return True
  156.  
  157.  
  158.  
  159. class ExtendedMaildir(maildir.MaildirMailbox):
  160.  
  161. """
  162.  
  163. Extends maildir.MaildirMailbox to expose more
  164.  
  165. of the underlying filename data
  166.  
  167. """
  168.  
  169. def __iter__(self):
  170.  
  171. "iterates through the full paths of all messages in the maildir"
  172.  
  173. return iter(self.list)
  174.  
  175.  
  176.  
  177. def __len__(self):
  178.  
  179. return len(self.list)
  180.  
  181.  
  182.  
  183. def __getitem__(self, i):
  184.  
  185. return self.list[i]
  186.  
  187.  
  188.  
  189. def deleteMessage(self, filename):
  190.  
  191. index = self.list.index(filename)
  192.  
  193. os.remove(filename)
  194.  
  195. del(self.list[index])
  196.  
  197.  
  198.  
  199. class IMAPMailbox(object):
  200.  
  201. implements(imap4.IMailbox)
  202.  
  203.  
  204.  
  205. def __init__(self, path):
  206.  
  207. self.maildir = ExtendedMaildir(path)
  208.  
  209. self.metadataFile = os.path.join(path, '.imap-metadata.pickle')
  210.  
  211. if os.path.exists(self.metadataFile):
  212.  
  213. self.metadata = pickle.load(file(self.metadataFile, 'r+b'))
  214.  
  215. else:
  216.  
  217. self.metadata = {}
  218.  
  219. self.initMetadata()
  220.  
  221. self.listeners = []
  222.  
  223. self._assignUIDs()
  224.  
  225.  
  226.  
  227. def initMetadata(self):
  228.  
  229. if not self.metadata.has_key('flags'):
  230.  
  231. self.metadata['flags'] = {} # dict of message IDs to flags
  232.  
  233. if not self.metadata.has_key('uidvalidity'):
  234.  
  235. # create a unique integer ID to identify this version of
  236.  
  237. # the mailbox, so the client could tell if it was deleted
  238.  
  239. # and replaced by a different mailbox with the same name
  240.  
  241. self.metadata['uidvalidity'] = random.randint(1000000, 9999999)
  242.  
  243. if not self.metadata.has_key('uids'):
  244.  
  245. self.metadata['uids'] = {}
  246.  
  247. if not self.metadata.has_key('uidnext'):
  248.  
  249. self.metadata['uidnext'] = 1 # next UID to be assigned
  250.  
  251.  
  252.  
  253. def saveMetadata(self):
  254.  
  255. pickle.dump(self.metadata, file(self.metadataFile, 'w+b'))
  256.  
  257.  
  258.  
  259. def _assignUIDs(self):
  260.  
  261. # make sure every message has a uid
  262.  
  263. for messagePath in self.maildir:
  264.  
  265. messageFile = os.path.basename(messagePath)
  266.  
  267. if not self.metadata['uids'].has_key(messageFile):
  268.  
  269. self.metadata['uids'][messageFile] =
  270.  
  271. self.metadata['uidnext']
  272.  
  273. self.metadata['uidnext'] += 1
  274.  
  275. self.saveMetadata()
  276.  
  277.  
  278.  
  279. def getHierarchicalDelimiter(self):
  280.  
  281. return MAILBOXDELIMITER
  282.  
  283.  
  284.  
  285. def getFlags(self):
  286.  
  287. "return list of flags supported by this mailbox"
  288.  
  289. return [r'\Seen', r'\Unseen', r'Deleted',
  290.  
  291. r'\Flagged', r'\Answered', r'\Recent']
  292.  
  293.  
  294.  
  295. def getMessageCount(self):
  296.  
  297. return len(self.maildir)
  298.  
  299.  
  300.  
  301. def getRecentCount(self):
  302.  
  303. return 0
  304.  
  305.  
  306.  
  307. def getUnseenCount(self):
  308.  
  309. def messageIsUnseen(filename):
  310.  
  311. filename = os.path.basename(filename)
  312.  
  313. uid = self.metadata['uids'].get(filename)
  314.  
  315. flags = self.metadata['flags'].get(uid, [])
  316.  
  317. if not r'\Seen' in flags:
  318.  
  319. return True
  320.  
  321. return len(filter(messageIsUnseen, self.maildir))
  322.  
  323.  
  324.  
  325. def isWriteable(self):
  326.  
  327. return True
  328.  
  329.  
  330.  
  331. def getUIDValidity(self):
  332.  
  333. return self.metadata['uidvalidity']
  334.  
  335.  
  336.  
  337. def getUID(self, messageNum):
  338.  
  339. filename = os.path.basename(self.maildir[messageNum-1])
  340.  
  341. return self.metadata['uids'][filename]
  342.  
  343.  
  344.  
  345. def getUIDNext(self):
  346.  
  347. return self.folder.metadata['uidnext']
  348.  
  349.  
  350.  
  351. def _uidMessageSetToSeqDict(self, messageSet):
  352.  
  353. """
  354.  
  355. take a MessageSet object containing UIDs, and return
  356.  
  357. a dictionary mapping sequence numbers to filenames
  358.  
  359. """
  360.  
  361. # if messageSet.last is None, it means 'the end', and needs to
  362.  
  363. # be set to a sane high number before attempting to iterate
  364.  
  365. # through the MessageSet
  366.  
  367. if not messageSet.last:
  368.  
  369. messageSet.last = self.metadata['uidnext']
  370.  
  371. allUIDs = []
  372.  
  373. for filename in self.maildir:
  374.  
  375. shortFilename = os.path.basename(filename)
  376.  
  377. allUIDs.append(self.metadata['uids'][shortFilename])
  378.  
  379. allUIDs.sort()
  380.  
  381. seqMap = {}
  382.  
  383. for uid in messageSet:
  384.  
  385. # the message set covers a span of UIDs. not all of them
  386.  
  387. # will necessarily exist, so check each one for validity
  388.  
  389. if uid in allUIDs:
  390.  
  391. sequence = allUIDs.index(uid)+1
  392.  
  393. seqMap[sequence] = self.maildir[sequence-1]
  394.  
  395. return seqMap
  396.  
  397.  
  398.  
  399. def _seqMessageSetToSeqDict(self, messageSet):
  400.  
  401. """
  402.  
  403. take a MessageSet object containing message sequence numbers,
  404.  
  405. and return a dictionary mapping sequence number to filenames
  406.  
  407. """
  408.  
  409. # if messageSet.last is None, it means 'the end', and needs to
  410.  
  411. # be set to a sane high number before attempting to iterate
  412.  
  413. # through the MessageSet
  414.  
  415. if not messageSet.last: messageSet.last = len(self.maildir)-1
  416.  
  417. seqMap = {}
  418.  
  419. for messageNo in messageSet:
  420.  
  421. seqMap[messageNo] = self.maildir[messageNo-1]
  422.  
  423. return seqMap
  424.  
  425.  
  426.  
  427. def fetch(self, messages, uid):
  428.  
  429. if uid:
  430.  
  431. messagesToFetch = self._uidMessageSetToSeqDict(messages)
  432.  
  433. else:
  434.  
  435. messagesToFetch = self._seqMessageSetToSeqDict(messages)
  436.  
  437. for seq, filename in messagesToFetch.items():
  438.  
  439. uid = self.getUID(seq)
  440.  
  441. flags = self.metadata['flags'].get(uid, [])
  442.  
  443. yield seq, MaildirMessage(file(filename).read(), uid, flags)
  444.  
  445.  
  446.  
  447. def addListener(self, listener):
  448.  
  449. self.listeners.append(listener)
  450.  
  451. return True
  452.  
  453.  
  454.  
  455. def removeListener(self, listener):
  456.  
  457. self.listeners.remove(listener)
  458.  
  459. return True
  460.  
  461.  
  462.  
  463. def requestStatus(self, path):
  464.  
  465. return imap4.statusRequestHelper(self, path)
  466.  
  467.  
  468.  
  469. def addMessage(self, msg, flags=None, date=None):
  470.  
  471. if flags is None: flags = []
  472.  
  473. return self.maildir.appendMessage(msg).addCallback(
  474.  
  475. self._addedMessage, flags)
  476.  
  477.  
  478.  
  479. def _addedMessage(self, _, flags):
  480.  
  481. # the first argument is the value returned from
  482.  
  483. # MaildirMailbox.appendMessage. It doesn't contain any meaningful
  484.  
  485. # information and can be discarded. Using the name "_" is a Twisted
  486.  
  487. # idiom for unimportant return values.
  488.  
  489. self._assignUIDs()
  490.  
  491. messageFile = os.path.basename(self.maildir[-1])
  492.  
  493. messageID = self.metadata['uids'][messageFile]
  494.  
  495. self.metadata['flags'][messageID] = flags
  496.  
  497. self.saveMetadata()
  498.  
  499.  
  500.  
  501. def store(self, messageSet, flags, mode, uid):
  502.  
  503. if uid:
  504.  
  505. messages = self._uidMessageSetToSeqDict(messageSet)
  506.  
  507. else:
  508.  
  509. messages = self._seqMessageSetToSeqDict(messageSet)
  510.  
  511. setFlags = {}
  512.  
  513. for seq, filename in messages.items():
  514.  
  515. uid = self.getUID(seq)
  516.  
  517. if mode == 0: # replace flags
  518.  
  519. messageFlags = self.metadata['flags'][uid] = flags
  520.  
  521. else:
  522.  
  523. messageFlags = self.metadata['flags'].setdefault(uid, [])
  524.  
  525. for flag in flags:
  526.  
  527. # mode 1 is append, mode -1 is delete
  528.  
  529. if mode == 1 and not messageFlags.count(flag):
  530.  
  531. messageFlags.append(flag)
  532.  
  533. elif mode == -1 and messageFlags.count(flag):
  534.  
  535. messageFlags.remove(flag)
  536.  
  537. setFlags[seq] = messageFlags
  538.  
  539. self.saveMetadata()
  540.  
  541. return setFlags
  542.  
  543.  
  544.  
  545. def expunge(self):
  546.  
  547. "remove all messages marked for deletion"
  548.  
  549. removed = []
  550.  
  551. for filename in self.maildir:
  552.  
  553. uid = self.metadata['uids'].get(os.path.basename(filename))
  554.  
  555. if r"Deleted" in self.metadata['flags'].get(uid, []):
  556.  
  557. self.maildir.deleteMessage(filename)
  558.  
  559. # you could also throw away the metadata here
  560.  
  561. removed.append(uid)
  562.  
  563. return removed
  564.  
  565.  
  566.  
  567. def destroy(self):
  568.  
  569. "complete remove the mailbox and all its contents"
  570.  
  571. raise imap4.MailboxException("Permission denied.")
  572.  
  573.  
  574.  
  575. from cStringIO import StringIO
  576.  
  577. import email
  578.  
  579.  
  580.  
  581. class MaildirMessagePart(object):
  582.  
  583. implements(imap4.IMessagePart)
  584.  
  585.  
  586.  
  587. def __init__(self, mimeMessage):
  588.  
  589. self.message = mimeMessage
  590.  
  591. self.data = str(self.message)
  592.  
  593.  
  594.  
  595. def getHeaders(self, negate, *names):
  596.  
  597. """
  598.  
  599. Return a dict mapping header name to header value. If *names
  600.  
  601. is empty, match all headers; if negate is true, return only
  602.  
  603. headers _not_ listed in *names.
  604.  
  605. """
  606.  
  607. if not names: names = self.message.keys()
  608.  
  609. headers = {}
  610.  
  611. if negate:
  612.  
  613. for header in self.message.keys():
  614.  
  615. if header.upper() not in names:
  616.  
  617. headers[header.lower()] = self.message.get(header, '')
  618.  
  619. else:
  620.  
  621. for name in names:
  622.  
  623. headers[name.lower()] = self.message.get(name, '')
  624.  
  625. return headers
  626.  
  627.  
  628.  
  629. def getBodyFile(self):
  630.  
  631. "return a file-like object containing this message's body"
  632.  
  633. bodyData = str(self.message.get_payload())
  634.  
  635. return StringIO(bodyData)
  636.  
  637.  
  638.  
  639. def getSize(self):
  640.  
  641. return len(self.data)
  642.  
  643.  
  644.  
  645. def getInternalDate(self):
  646.  
  647. return self.message.get('Date', '')
  648.  
  649.  
  650.  
  651. def isMultipart(self):
  652.  
  653. return self.message.is_multipart()
  654.  
  655.  
  656.  
  657. def getSubPart(self, partNo):
  658.  
  659. return MaildirMessagePart(self.message.get_payload(partNo))
  660.  
  661.  
  662.  
  663. class MaildirMessage(MaildirMessagePart):
  664.  
  665. implements(imap4.IMessage)
  666.  
  667.  
  668.  
  669. def __init__(self, messageData, uid, flags):
  670.  
  671. self.data = messageData
  672.  
  673. self.message = email.message_from_string(self.data)
  674.  
  675. self.uid = uid
  676.  
  677. self.flags = flags
  678.  
  679.  
  680.  
  681. def getUID(self):
  682.  
  683. return self.uid
  684.  
  685.  
  686.  
  687. def getFlags(self):
  688.  
  689. return self.flags
  690.  
  691.  
  692.  
  693. class MailUserRealm(object):
  694.  
  695. implements(portal.IRealm)
  696.  
  697. avatarInterfaces = {
  698.  
  699. imap4.IAccount: IMAPUserAccount,
  700.  
  701. }
  702.  
  703.  
  704.  
  705. def __init__(self, baseDir):
  706.  
  707. self.baseDir = baseDir
  708.  
  709.  
  710.  
  711. def requestAvatar(self, avatarId, mind, *interfaces):
  712.  
  713. for requestedInterface in interfaces:
  714.  
  715. if self.avatarInterfaces.has_key(requestedInterface):
  716.  
  717. # make sure the user dir exists (avatarId is username)
  718.  
  719. userDir = os.path.join(self.baseDir, avatarId)
  720.  
  721. if not os.path.exists(userDir):
  722.  
  723. os.mkdir(userDir)
  724.  
  725. # return an instance of the correct class
  726.  
  727. avatarClass = self.avatarInterfaces[requestedInterface]
  728.  
  729. avatar = avatarClass(userDir)
  730.  
  731. # null logout function: take no arguments and do nothing
  732.  
  733. logout = lambda: None
  734.  
  735. return defer.succeed((requestedInterface, avatar, logout))
  736.  
  737.  
  738.  
  739. # none of the requested interfaces was supported
  740.  
  741. raise KeyError("None of the requested interfaces is supported")
  742.  
  743.  
  744.  
  745. def passwordFileToDict(filename):
  746.  
  747. passwords = {}
  748.  
  749. for line in file(filename):
  750.  
  751. if line and line.count(':'):
  752.  
  753. username, password = line.strip().split(':')
  754.  
  755. passwords[username] = password
  756.  
  757. return passwords
  758.  
  759.  
  760.  
  761. class CredentialsChecker(object):
  762.  
  763. implements(checkers.ICredentialsChecker)
  764.  
  765. credentialInterfaces = (credentials.IUsernamePassword,
  766.  
  767. credentials.IUsernameHashedPassword)
  768.  
  769.  
  770.  
  771. def __init__(self, passwords):
  772.  
  773. "passwords: a dict-like object mapping usernames to passwords"
  774.  
  775. self.passwords = passwords
  776.  
  777.  
  778.  
  779. def requestAvatarId(self, credentials):
  780.  
  781. """
  782.  
  783. check to see if the supplied credentials authenticate.
  784.  
  785. if so, return an 'avatar id', in this case the name of
  786.  
  787. the IMAP user.
  788.  
  789. The supplied credentials will implement one of the classes
  790.  
  791. in self.credentialInterfaces. In this case both
  792.  
  793. IUsernamePassword and IUsernameHashedPassword have a
  794.  
  795. checkPassword method that takes the real password and checks
  796.  
  797. it against the supplied password.
  798.  
  799. """
  800.  
  801. username = credentials.username
  802.  
  803. if self.passwords.has_key(username):
  804.  
  805. realPassword = self.passwords[username]
  806.  
  807. checking = defer.maybeDeferred(
  808.  
  809. credentials.checkPassword, realPassword)
  810.  
  811. # pass result of checkPassword, and the username that was
  812.  
  813. # being authenticated, to self._checkedPassword
  814.  
  815. checking.addCallback(self._checkedPassword, username)
  816.  
  817. return checking
  818.  
  819. else:
  820.  
  821. raise credError.UnauthorizedLogin("No such user")
  822.  
  823.  
  824.  
  825. def _checkedPassword(self, matched, username):
  826.  
  827. if matched:
  828.  
  829. # password was correct
  830.  
  831. return username
  832.  
  833. else:
  834.  
  835. raise credError.UnauthorizedLogin("Bad password")
  836.  
  837.  
  838.  
  839. class IMAPServerProtocol(imap4.IMAP4Server):
  840.  
  841. "Subclass of imap4.IMAP4Server that adds debugging."
  842.  
  843. debug = True
  844.  
  845.  
  846.  
  847. def lineReceived(self, line):
  848.  
  849. if self.debug:
  850.  
  851. print "CLIENT:", line
  852.  
  853. imap4.IMAP4Server.lineReceived(self, line)
  854.  
  855.  
  856.  
  857. def sendLine(self, line):
  858.  
  859. imap4.IMAP4Server.sendLine(self, line)
  860.  
  861. if self.debug:
  862.  
  863. print "SERVER:", line
  864.  
  865.  
  866.  
  867. class IMAPFactory(protocol.Factory):
  868.  
  869. protocol = IMAPServerProtocol
  870.  
  871. portal = None # placeholder
  872.  
  873.  
  874.  
  875. def buildProtocol(self, address):
  876.  
  877. p = self.protocol()
  878.  
  879. p.portal = self.portal
  880.  
  881. p.factory = self
  882.  
  883. return p
  884.  
  885.  
  886.  
  887. if __name__ == "__main__":
  888.  
  889. import sys
  890.  
  891. dataDir = sys.argv[1]
  892.  
  893.  
  894.  
  895. portal = portal.Portal(MailUserRealm(dataDir))
  896.  
  897. passwordFile = os.path.join(dataDir, 'passwords.txt')
  898.  
  899. passwords = passwordFileToDict(passwordFile)
  900.  
  901. passwordChecker = CredentialsChecker(passwords)
  902.  
  903. portal.registerChecker(passwordChecker)
  904.  
  905.  
  906.  
  907. factory = IMAPFactory()
  908.  
  909. factory.portal = portal
  910.  
  911.  
  912.  
  913. reactor.listenTCP(143, factory)
  914.  
  915. reactor.run()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement