Ejemplo n.º 1
0
    def update_mailbox(self,
                       id,
                       name=None,
                       parentId=None,
                       isSubscribed=None,
                       sortOrder=None,
                       **update):
        mailbox = self.mailboxes.get(id, None)
        if not mailbox:
            raise errors.notFound('mailbox not found')
        imapname = mailbox['imapname']

        if (name is not None and name != mailbox['name']) or \
           (parentId is not None and parentId != mailbox['parentId']):
            if not name:
                raise errors.invalidProperties('name is required')
            newimapname = self.mailbox_imapname(parentId, name)
            res = self.imap.rename_folder(imapname, newimapname)
            if b'NO' in res or b'BAD' in res:
                raise errors.serverFail(res.encode())

        if isSubscribed is not None and isSubscribed != mailbox['isSubscribed']:
            if isSubscribed:
                res = self.imap.subscribe_folder(imapname)
            else:
                res = self.imap.unsubscribe_folder(imapname)
            if b'NO' in res or b'BAD' in res:
                raise errors.serverFail(res.encode())

        if sortOrder is not None and sortOrder != mailbox['sortOrder']:
            # TODO: update in persistent storage
            mailbox['sortOrder'] = sortOrder
        self.sync_mailboxes()
Ejemplo n.º 2
0
    async def update_email(self, msg, patch):
        values = patch.pop('keywords', {})
        store = {
            True: [keyword2flag(k) for k, v in values.items() if v],
            False: [keyword2flag(k) for k, v in values.items() if not v]
        }
        values = patch.pop('mailboxIds', {})
        mids = {
            True: [k for k, v in values.items() if v],
            False: [k for k, v in values.items() if not v]
        }

        for path, value in patch.items():
            prop, _, key = path.partition('/')
            if prop == 'keywords':
                store[bool(value)].append(keyword2flag(key))
            elif prop == 'mailboxIds':
                mids[bool(value)].append(key)
            else:
                raise errors.invalidArguments(f"Unknown update {path}")

        uid = str(self.parse_email_id(msg['id']))
        for add, flags in store.items():
            flags = f"({' '.join(flags)})"
            ok, lines = await self.imap.uid_store(
                uid, '+FLAGS' if add else '-FLAGS', flags)
            if ok != 'OK':
                raise errors.serverFail('\n'.join(lines))
            for seq, data in parse_fetch(lines[:-1]):
                if uid == data['UID']:
                    msg['FLAGS'] = data['FLAGS']
                    msg.pop('keywords', None)

        # if msg is already there, ignore invalid False folders
        if (mids[True] or mids[False]) and \
            msg['mailboxIds'] != mids[True]:
            if len(mids[True]) > 1 or \
               len(mids[False]) > 1 or \
               msg['mailboxIds'] != mids[False]:
                raise errors.tooManyMailboxes(
                    "Email must be always in exactly 1 mailbox")
            try:
                mailbox_to = self.mailboxes[mids[True][0]]
            except KeyError:
                raise errors.notFound('Mailbox not found')
            ok, lines = await self.imap.uid_move(
                uid, quoted(mailbox_to['imapname']))
            if ok != 'OK':
                raise errors.serverFail('\n'.join(lines))
Ejemplo n.º 3
0
 async def download(self, blobId):
     async with self.http.get(f"{self.base}{self.id}/{blobId}") as res:
         if res.status == 200:
             return await res.read()
         elif res.status // 100 == 5:
             raise errors.serverFail()
     raise errors.notFound(f'Blob {blobId} not found')
Ejemplo n.º 4
0
 def destroy_mailbox(self, id):
     mailbox = self.mailboxes.get(id, None)
     if not mailbox:
         raise errors.notFound('mailbox not found')
     res = self.imap.delete_folder(mailbox['imapname'])
     if b'NO' in res or b'BAD' in res:
         raise errors.serverFail(res.encode())
     mailbox['deleted'] = datetime.now().timestamp()
     self.sync_mailboxes()
Ejemplo n.º 5
0
def api_Blob_copy(request, fromAccountId, accountId, blobIds):
    fromAccount = request['user'].get_account(fromAccountId)
    account = request['user'].get_account(accountId)
    return {
        'fromAccountId': fromAccountId,
        'accountId': accountId,
        'copied': None,
        'notCopied':
        {id: errors.serverFail('Blob/copy not implemented')
         for id in blobIds},
    }
Ejemplo n.º 6
0
    async def fill_emails(self, properties=(), ids=None):
        """Fills self.emails with required properties"""

        try:
            fields = {FIELDS_MAP[prop] for prop in properties}
        except KeyError as e:
            raise errors.invalidArguments(f'Property not recognized: {e}')

        if 'BODY.PEEK[]' in fields:  # remove redundand fields
            fields.discard('BODY.PEEK[HEADER]')
            fields.discard('RFC822.SIZE')

        fetch_uids = set()
        fetch_fields = set()
        for id in ids:
            try:
                uid = self.parse_email_id(id)
            except ValueError:
                continue
            msg = self.emails.get(id, None)
            if msg is None:
                fetch_uids.add(uid)
                fetch_fields = fields
            else:
                missing = fields - msg.keys()
                if missing:
                    fetch_uids.add(uid)
                    fetch_fields.update(missing)

        if not fetch_fields:
            return
        fetch_fields.add('UID')
        fetch_uids = encode_messageset(fetch_uids).decode()
        ok, lines = await self.imap.uid_fetch(
            fetch_uids, "(%s)" % (' '.join(fetch_fields)))
        if ok != 'OK':
            raise errors.serverFail(lines[0])
        for seq, data in parse_fetch(lines[:-1]):
            id = self.format_email_id(data['UID'])
            msg = self.emails.get(id, None)
            if not msg:
                msg = ImapEmail(id=id)
                self.emails[id] = msg
            if 'mailboxIds' in properties:
                try:
                    imapname = unquoted(data['X-MAILBOX'])
                except KeyError:
                    # don't know why sometimes Dovecot returns additional
                    # FETCH with duplicate UID with only MODSEQ
                    if msg['mailboxIds']:
                        continue
                msg['mailboxIds'] = [self.byimapname[imapname]['id']]
            msg.update(data)
Ejemplo n.º 7
0
 async def _imap_append(self,
                        body,
                        imapname='INBOX',
                        flags=None,
                        data=None):
     ok, lines = await self.imap.append(body, imapname, flags)
     match = re.search(r'\[APPENDUID (\d+) (\d+)\]', lines[-1])
     # ensure refreshed folder view
     ok, lines = await self.imap.noop()
     ok, lines = await self.imap.uid_search(
         f"X-REAL-UID {match[2]} X-MAILBOX {imapname}", ret='ALL')
     search = parse_esearch(lines)
     ok, lines = await self.imap.uid_fetch(search['ALL'], "(UID X-GUID)")
     for seq, fetch in parse_fetch(lines[:-1]):
         return int(fetch['UID']), fetch['X-GUID']
     raise errors.serverFail("Couldn't fetch UID X-GUID")
Ejemplo n.º 8
0
    def create_mailbox(self,
                       name=None,
                       parentId=None,
                       isSubscribed=True,
                       **kwargs):
        if not name:
            raise errors.invalidProperties('name is required')
        imapname = self.mailbox_imapname(parentId, name)
        # TODO: parse returned MAILBOXID
        try:
            res = self.imap.create_folder(imapname)
        except IMAPClientError as e:
            desc = str(e)
            if '[ALREADYEXISTS]' in desc:
                raise errors.invalidArguments(desc)
        except Exception:
            raise errors.serverFail(res.decode())

        if not isSubscribed:
            self.imap.unsubscribe_folder(imapname)

        status = self.imap.folder_status(imapname, ['UIDVALIDITY'])
        self.sync_mailboxes()
        return f"f{status[b'UIDVALIDITY']}"
Ejemplo n.º 9
0
    async def email_get(self,
                        idmap,
                        ids=None,
                        properties=None,
                        bodyProperties=None,
                        fetchTextBodyValues=False,
                        fetchHTMLBodyValues=False,
                        fetchAllBodyValues=False,
                        maxBodyValueBytes=0):
        """https://jmap.io/spec-mail.html#emailget"""
        lst = []
        notFound = []
        fill_props = set()
        header_props = set()
        if properties:
            for prop in properties:
                m = header_prop_re.match(prop)
                if m is None:
                    fill_props.add(prop)
                else:
                    header_props.add(m.group(0, 1, 2, 3))
                    fill_props.add('headers')
            if 'body' in fill_props:
                fill_props.remove('body')
                fill_props.update(('textBody', 'htmlBody'))
        else:
            properties = ALL_PROPERTIES
            fill_props = set(properties)

        if bodyProperties is None:
            bodyProperties = ALL_BODY_PROPERTIES

        if header_props and 'headers' not in properties:
            fill_props.remove('headers')

        if ids is None:
            # get MAX_OBJECTS_IN_GET
            ok, lines = await self.imap.search('ALL', ret='ALL')
            if ok != 'OK':
                raise errors.serverFail('\n'.join(lines))
            uids = iter_messageset(parse_esearch(lines).get('ALL', ''))
            ids = (self.format_email_id(uid) for uid in uids)
            ids = tuple(itertools.islice(ids, MAX_OBJECTS_IN_GET))
        elif len(ids) > MAX_OBJECTS_IN_GET:
            raise errors.tooLarge(
                'Requested more than {MAX_OBJECTS_IN_GET} ids')
        else:
            ids = [idmap.get(id) for id in ids]

        await self.fill_emails(fill_props, ids)

        for id in ids:
            try:
                msg = self.emails[id]
            except KeyError:
                notFound.append(id)
                continue

            # Fill most of msg properties except header:*
            data = {prop: msg[prop] for prop in fill_props}
            data['id'] = msg['id']
            if 'textBody' in msg and 'htmlBody' not in msg and not msg[
                    'textBody']:
                data['textBody'] = htmltotext(msg['htmlBody'])
            if 'bodyValues' in properties:
                # bug: jmap-demo-webmail needs all bodyValues even when fetchHTMLBodyValues=True
                if fetchHTMLBodyValues:
                    data['bodyValues'] = {
                        k: v
                        for k, v in msg['bodyValues'].items()
                        if v['type'] == 'text/html'
                    }
                elif fetchTextBodyValues:
                    data['bodyValues'] = {
                        k: v
                        for k, v in msg['bodyValues'].items()
                        if v['type'] == 'text/plain'
                    }
                elif fetchAllBodyValues:
                    data['bodyValues'] = msg['bodyValues']
                if maxBodyValueBytes:
                    for k, bodyValue in data['bodyValues'].items():
                        if len(bodyValue['value']) > maxBodyValueBytes:
                            bodyValue = {k: v for k, v in bodyValue.items()}
                            bodyValue['value'] = bodyValue[
                                'value'][:maxBodyValueBytes]
                            bodyValue['isTruncated'] = True,
                            data['bodyValues'][k] = bodyValue

            for prop, name, form, getall in header_props:
                try:
                    func = HEADER_FORMS[form]
                except KeyError:
                    raise errors.invalidProperties(
                        f'Unknown header-form {form} in {prop}')

                name = name.lower()
                if getall:
                    data[prop] = [
                        func(h['value']) for h in msg['headers']
                        if h['name'].lower() == name
                    ]
                else:
                    data[prop] = func(msg.get_header(name))

            lst.append(data)

        return {
            'accountId': self.id,
            'list': lst,
            'state': await self.email_state(),
            'notFound': list(notFound),
        }
Ejemplo n.º 10
0
    async def mailbox_set(self,
                          idmap,
                          ifInState=None,
                          create=None,
                          update=None,
                          destroy=None,
                          onDestroyRemoveEmails=False):
        """https://jmap.io/spec-mail.html#mailboxset"""
        oldState = await self.mailbox_state()
        if ifInState is not None and ifInState != oldState:
            raise errors.stateMismatch()

        # CREATE
        created = {}
        notCreated = {}
        created_imapnames = {}
        for cid, mailbox in (create or {}).items():
            mbox = ImapMailbox(mailbox)
            mbox.db = self
            try:
                imapname = mbox['imapname']
            except KeyError:
                raise errors.notFound("Parent mailbox not found")
            try:
                ok, lines = await self.imap.create(quoted(imapname))
                if ok != 'OK':
                    if '[ALREADYEXISTS]' in lines[0]:
                        raise errors.invalidArguments(lines[0])
                    else:
                        raise errors.serverFail(lines[0])
                # OBJECTID extension returns MAILBOXID on create
                match = re.search(r'\[MAILBOXID \(([^)]+)\)\]', lines[0])
                if match:
                    id = match.group(1)
                    mbox[id] = id
                    mbox['sep'] = '/'
                    mbox['flags'] = set()
                    self.mailboxes[id] = mbox
                    self.byimapname[imapname] = mbox
                    created[cid] = {'id': id}
                else:
                    # set created[cid] after sync_mailboxes()
                    created_imapnames[cid] = imapname
                if not mailbox.get('isSubscribed', True):
                    ok, lines = await self.imap.unsubscribe(imapname)
                    # TODO: handle failed unsubscribe
            except KeyError:
                notCreated[cid] = errors.invalidArguments().to_dict()
            except errors.JmapError as e:
                notCreated[cid] = e.to_dict()

        # UPDATE
        updated = {}
        notUpdated = {}
        for id, update in (update or {}).items():
            try:
                mailbox = self.mailboxes.get(id, None)
                if not mailbox or mailbox['deleted']:
                    raise errors.notFound(f'Mailbox {id} not found')
                await self.update_mailbox(mailbox, update)
                updated[id] = update
            except errors.JmapError as e:
                notUpdated[id] = e.to_dict()

        # DESTROY
        destroyed = []
        notDestroyed = {}
        for id in destroy or ():
            try:
                mailbox = self.mailboxes.get(id, None)
                if not mailbox or mailbox['deleted']:
                    raise errors.notFound('mailbox not found')
                ok, lines = await self.imap.delete(quoted(mailbox['imapname']))
                if ok != 'OK':
                    raise errors.serverFail(lines[0])
                mailbox['deleted'] = True
                destroyed.append(id)
            except errors.JmapError as e:
                notDestroyed[id] = e.to_dict()

        await self.sync_mailboxes({'id', 'imapname'})
        for cid, imapname in created_imapnames.values():
            mbox = self.byimapname.get(imapname, None)
            if mbox:
                created[cid] = {'id': mbox['id']}
                idmap.set(cid, mbox['id'])
            else:
                notCreated[cid] = errors.serverFail().to_dict()

        return {
            'accountId': self.id,
            'oldState': oldState,
            'newState': await self.mailbox_state(),
            'created': created,
            'notCreated': notCreated,
            'updated': updated,
            'notUpdated': notUpdated,
            'destroyed': destroyed,
            'notDestroyed': notDestroyed,
        }