Skip to content

Commit

Permalink
IMAP implementation
Browse files Browse the repository at this point in the history
This is a very limited implementation of IMAPv4rev1 (rfc3501). But this
is sufficient for mutt, isync and thunderbird clients.

Signed-off-by: Alexey Gladkov <gladkov.alexey@gmail.com>
  • Loading branch information
legionus committed Nov 13, 2023
1 parent 743a019 commit 95f6236
Show file tree
Hide file tree
Showing 12 changed files with 1,554 additions and 49 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: "Check mypy"
run: |
mypy --non-interactive --install-types -c 'import tabulate'
mypy --strict jiramail/*.py
find jiramail -type f -name '*.py' -a \! -name '*_tab.py' | xargs -r mypy --strict
- name: "Check pylint"
run: pylint --disable=R --disable=W0603,W0621,W0718 --disable=C0103,C0114,C0115,C0116,C0301,C0415,C3001 jiramail/*.py
13 changes: 13 additions & 0 deletions jiramail/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,29 @@ def __init__(self, path: str):
logger.debug("openning the mailbox `%s' ...", path)

self.mbox = mailbox.mbox(path)
self.path = os.path.abspath(os.path.expanduser(path))
self.n_msgs = 0
self.msgid = {}

for key in self.mbox.iterkeys():
mail = self.mbox.get_message(key)
if "Message-Id" in mail:
msg_id = mail.get("Message-Id")
self.msgid[msg_id] = True
self.n_msgs += 1

logger.info("mailbox is ready")

def get_message(self, key: str) -> mailbox.mboxMessage:
return self.mbox.get_message(key)

def del_message(self, key: str) -> None:
mail = self.get_message(key)
msg_id = mail.get("Message-Id")

self.mbox.remove(key)
del self.msgid[msg_id]

def update_message(self, key: str, mail: email.message.Message) -> None:
self.mbox.update([(key, mail)])

Expand All @@ -105,6 +115,9 @@ def append(self, mail: email.message.Message) -> None:
def iterkeys(self) -> Iterator[Any]:
return self.mbox.iterkeys()

def sync(self) -> None:
self.mbox.flush()

def close(self) -> None:
self.mbox.close()

Expand Down
46 changes: 46 additions & 0 deletions jiramail/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2023 Alexey Gladkov <gladkov.alexey@gmail.com>

__author__ = 'Alexey Gladkov <gladkov.alexey@gmail.com>'

import base64
import binascii
import hashlib
import hmac
import os
import random
import time

from typing import Callable, Tuple, Any

import jiramail

logger = jiramail.logger

def cram_md5(user: str, password: str, interact: Callable[[Any, str], str], data: Any) -> Tuple[bool, str]:
pid = os.getpid()
now = time.time_ns()
rnd = random.randrange(2**32 - 1)
shared = f"<{pid}.{now}.{rnd}@jiramail>"

line = interact(data, base64.b64encode(shared.encode()).decode())

try:
buf = base64.standard_b64decode(line).decode()
except binascii.Error:
return (False, "couldn't decode your credentials")

fields = buf.split(" ")

if len(fields) != 2:
return (False, "wrong number of fields in the token")

hexdigest = hmac.new(password.encode(),
shared.encode(),
hashlib.md5).hexdigest()

if hmac.compare_digest(user, fields[0]) and hmac.compare_digest(hexdigest, fields[1]):
return (True, "authentication successful")

return (False, "authenticate failure")
18 changes: 18 additions & 0 deletions jiramail/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ def cmd_smtp(cmdargs: argparse.Namespace) -> int:
return jiramail.smtp.main(cmdargs)


def cmd_imap(cmdargs: argparse.Namespace) -> int:
import jiramail.imap
return jiramail.imap.main(cmdargs)


def add_common_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument("-v", "--verbose",
dest="verbose", action='count', default=0,
Expand Down Expand Up @@ -184,6 +189,19 @@ def setup_parser() -> argparse.ArgumentParser:
help="path to mailbox to store a reply messages with the status of command execution.")
add_common_arguments(sp4)

# jiramail imap
sp5_description = """\
imap server
"""
sp5 = subparsers.add_parser("imap",
description=sp3_description,
help=sp5_description,
epilog=epilog,
add_help=False)
sp5.set_defaults(func=cmd_imap)
add_common_arguments(sp5)


return parser


Expand Down
Loading

0 comments on commit 95f6236

Please sign in to comment.