Source code for synapse.lib.stormlib.imap

import asyncio

import aioimaplib

import synapse.exc as s_exc
import synapse.common as s_common
import synapse.lib.stormtypes as s_stormtypes

[docs] async def run_imap_coro(coro): ''' Raises or returns data. ''' try: status, data = await coro except asyncio.TimeoutError: raise s_exc.StormRuntimeError(mesg='Timed out waiting for IMAP server response.') from None if status == 'OK': return data try: mesg = data[0].decode() except (TypeError, AttributeError, IndexError, UnicodeDecodeError): mesg = 'IMAP server returned an error' raise s_exc.StormRuntimeError(mesg=mesg, status=status)
[docs] @s_stormtypes.registry.registerLib class ImapLib(s_stormtypes.Lib): ''' A Storm library to connect to an IMAP server. ''' _storm_locals = ( { 'name': 'connect', 'desc': ''' Open a connection to an IMAP server. This method will wait for a "hello" response from the server before returning the ``inet:imap:server`` instance. ''', 'type': { 'type': 'function', '_funcname': 'connect', 'args': ( {'type': 'str', 'name': 'host', 'desc': 'The IMAP hostname.'}, {'type': 'int', 'name': 'port', 'default': 993, 'desc': 'The IMAP server port.'}, {'type': 'int', 'name': 'timeout', 'default': 30, 'desc': 'The time to wait for all commands on the server to execute.'}, {'type': 'bool', 'name': 'ssl', 'default': True, 'desc': 'Use SSL to connect to the IMAP server.'}, {'type': 'bool', 'name': 'ssl_verify', 'default': True, 'desc': 'Perform SSL/TLS verification.'}, ), 'returns': { 'type': 'inet:imap:server', 'desc': 'A new ``inet:imap:server`` instance.' }, }, }, ) _storm_lib_path = ('inet', 'imap', ) _storm_lib_perms = ( {'perm': ('storm', 'inet', 'imap', 'connect'), 'gate': 'cortex', 'desc': 'Controls connecting to external servers via imap.'}, )
[docs] def getObjLocals(self): return { 'connect': self.connect, }
[docs] async def connect(self, host, port=993, timeout=30, ssl=True, ssl_verify=True): self.runt.confirm(('storm', 'inet', 'imap', 'connect')) ssl = await s_stormtypes.tobool(ssl) host = await s_stormtypes.tostr(host) port = await s_stormtypes.toint(port) ssl_verify = await s_stormtypes.tobool(ssl_verify) timeout = await s_stormtypes.toint(timeout, noneok=True) if ssl: ctx = self.runt.snap.core.getCachedSslCtx(opts=None, verify=ssl_verify) imap_cli = aioimaplib.IMAP4_SSL(host=host, port=port, timeout=timeout, ssl_context=ctx) else: imap_cli = aioimaplib.IMAP4(host=host, port=port, timeout=timeout) async def fini(): # call protocol.logout() so fini() doesn't hang await s_common.wait_for(imap_cli.protocol.logout(), 5) self.runt.snap.onfini(fini) try: await imap_cli.wait_hello_from_server() except asyncio.TimeoutError: raise s_exc.StormRuntimeError(mesg='Timed out waiting for IMAP server hello.') from None return ImapServer(self.runt, imap_cli)
[docs] @s_stormtypes.registry.registerType class ImapServer(s_stormtypes.StormType): ''' An IMAP server for retrieving email messages. ''' _storm_locals = ( { 'name': 'list', 'desc': ''' List mailbox names. By default this method uses a reference_name and pattern to return all mailboxes from the root. ''', 'type': { 'type': 'function', '_funcname': 'list', 'args': ( {'type': 'str', 'name': 'reference_name', 'default': '""', 'desc': 'The mailbox reference name.'}, {'type': 'str', 'name': 'pattern', 'default': '*', 'desc': 'The pattern to filter by.'}, ), 'returns': { 'type': 'list', 'desc': 'An ($ok, $valu) tuple where $valu is a list of names if $ok=True.' }, }, }, { 'name': 'fetch', 'desc': ''' Fetch a message by UID in RFC822 format. The message is saved to the Axon, and a ``file:bytes`` node is returned. Examples: Fetch a message, save to the Axon, and yield ``file:bytes`` node:: yield $server.fetch("8182") ''', 'type': { 'type': 'function', '_funcname': 'fetch', 'args': ( {'type': 'str', 'name': 'uid', 'desc': 'The single message UID.'}, ), 'returns': { 'type': 'node', 'desc': 'The file:bytes node representing the message.' }, }, }, { 'name': 'login', 'desc': 'Login to the IMAP server.', 'type': { 'type': 'function', '_funcname': 'login', 'args': ( {'type': 'str', 'name': 'user', 'desc': 'The username to login with.'}, {'type': 'str', 'name': 'passwd', 'desc': 'The password to login with.'}, ), 'returns': { 'type': 'list', 'desc': 'An ($ok, $valu) tuple.' }, }, }, { 'name': 'search', 'desc': ''' Search for messages using RFC2060 syntax. Examples: Retrieve all messages:: ($ok, $uids) = $server.search("ALL") Search by FROM and SINCE:: ($ok, $uids) = $server.search("FROM", "[email protected]", "SINCE", "01-Oct-2021") Search by a subject substring:: ($ok, $uids) = $search.search("HEADER", "Subject", "An email subject") ''', 'type': { 'type': 'function', '_funcname': 'search', 'args': ( {'type': 'str', 'name': '*args', 'desc': 'A set of search criteria to use.'}, {'type': ['str', 'null'], 'name': 'charset', 'default': 'utf-8', 'desc': 'The CHARSET used for the search. May be set to $lib.null to disable CHARSET.'}, ), 'returns': { 'type': 'list', 'desc': 'An ($ok, $valu) tuple, where $valu is a list of UIDs if $ok=True.' }, }, }, { 'name': 'select', 'desc': 'Select a mailbox to use in subsequent commands.', 'type': { 'type': 'function', '_funcname': 'select', 'args': ( {'type': 'str', 'name': 'mailbox', 'default': 'INBOX', 'desc': 'The mailbox name to select.'}, ), 'returns': { 'type': 'list', 'desc': 'An ($ok, $valu) tuple.' }, }, }, { 'name': 'markSeen', 'desc': ''' Mark messages as seen by an RFC2060 UID message set. The command uses the +FLAGS.SILENT command and applies the \\Seen flag. Examples: Mark a single messsage as seen:: ($ok, $valu) = $server.markSeen("8182") Mark ranges of messages as seen:: ($ok, $valu) = $server.markSeen("1:3,6:9") ''', 'type': { 'type': 'function', '_funcname': 'markSeen', 'args': ( {'type': 'str', 'name': 'uid_set', 'desc': 'The UID message set to apply the flag to.'}, ), 'returns': { 'type': 'list', 'desc': 'An ($ok, $valu) tuple.' }, }, }, { 'name': 'delete', 'desc': ''' Mark an RFC2060 UID message as deleted and expunge the mailbox. The command uses the +FLAGS.SILENT command and applies the \\Deleted flag. The actual behavior of these commands are mailbox configuration dependent. Examples: Mark a single message as deleted and expunge:: ($ok, $valu) = $server.delete("8182") Mark ranges of messages as deleted and expunge:: ($ok, $valu) = $server.delete("1:3,6:9") ''', 'type': { 'type': 'function', '_funcname': 'delete', 'args': ( {'type': 'str', 'name': 'uid_set', 'desc': 'The UID message set to apply the flag to.'}, ), 'returns': { 'type': 'list', 'desc': 'An ($ok, $valu) tuple.' }, }, }, ) _storm_typename = 'inet:imap:server' def __init__(self, runt, imap_cli, path=None): s_stormtypes.StormType.__init__(self, path=path) self.runt = runt self.imap_cli = imap_cli self.locls.update(self.getObjLocals())
[docs] def getObjLocals(self): return { 'list': self.list, 'fetch': self.fetch, 'login': self.login, 'delete': self.delete, 'search': self.search, 'select': self.select, 'markSeen': self.markSeen, }
[docs] async def login(self, user, passwd): user = await s_stormtypes.tostr(user) passwd = await s_stormtypes.tostr(passwd) coro = self.imap_cli.login(user, passwd) await run_imap_coro(coro) return True, None
[docs] async def list(self, reference_name='""', pattern='*'): pattern = await s_stormtypes.tostr(pattern) reference_name = await s_stormtypes.tostr(reference_name) coro = self.imap_cli.list(reference_name, pattern) data = await run_imap_coro(coro) names = [] for item in data: if item == b'Success': break names.append(item.split(b' ')[-1].decode().strip('"')) return True, names
[docs] async def select(self, mailbox='INBOX'): mailbox = await s_stormtypes.tostr(mailbox) coro = self.imap_cli.select(mailbox=mailbox) await run_imap_coro(coro) return True, None
[docs] async def search(self, *args, charset='utf-8'): args = [await s_stormtypes.tostr(arg) for arg in args] charset = await s_stormtypes.tostr(charset, noneok=True) coro = self.imap_cli.uid_search(*args, charset=charset) data = await run_imap_coro(coro) uids = data[0].decode().split(' ') if data[0] else [] return True, uids
[docs] async def fetch(self, uid): # IMAP fetch accepts a message set (e.g. "1", "1:*", "1,2,3"), # however this method forces fetching a single uid # to prevent retrieving a very large blob of data. uid = await s_stormtypes.toint(uid) await self.runt.snap.core.getAxon() axon = self.runt.snap.core.axon coro = self.imap_cli.uid('FETCH', str(uid), '(RFC822)') data = await run_imap_coro(coro) size, sha256b = await axon.put(data[1]) props = await axon.hashset(sha256b) props['size'] = size props['mime'] = 'message/rfc822' filenode = await self.runt.snap.addNode('file:bytes', props['sha256'], props=props) return filenode
[docs] async def delete(self, uid_set): uid_set = await s_stormtypes.tostr(uid_set) coro = self.imap_cli.uid('STORE', uid_set, '+FLAGS.SILENT (\\Deleted)') await run_imap_coro(coro) coro = self.imap_cli.expunge() await run_imap_coro(coro) return True, None
[docs] async def markSeen(self, uid_set): uid_set = await s_stormtypes.tostr(uid_set) coro = self.imap_cli.uid('STORE', uid_set, '+FLAGS.SILENT (\\Seen)') await run_imap_coro(coro) return True, None