Skip to content

Commit

Permalink
Merge pull request #5 from smkent/dev
Browse files Browse the repository at this point in the history
Prepare waffles (now wafflesbot) for publishing to PyPI
  • Loading branch information
smkent authored Mar 2, 2022
2 parents d52403d + de681a0 commit aa92885
Show file tree
Hide file tree
Showing 12 changed files with 105 additions and 39 deletions.
70 changes: 68 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,66 @@
# waffles: Tech recruiter auto reply bot using [JMAP][jmap] via [jmapc][jmapc]
# wafflesbot: Email auto reply bot for [JMAP][jmap] mailboxes

waffles is in initial development.
[![PyPI](https://img.shields.io/pypi/v/wafflesbot)][pypi]
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wafflesbot)][pypi]
[![Build](https://img.shields.io/github/checks-status/smkent/waffles/master?label=build)][gh-actions]
[![codecov](https://codecov.io/gh/smkent/waffles/branch/master/graph/badge.svg)][codecov]
[![GitHub stars](https://img.shields.io/github/stars/smkent/waffles?style=social)][repo]

wafflesbot sends form replies to unreplied emails in a [JMAP][jmap] mailbox
(such as [Fastmail][fastmail]).

wafflesbot excels at automatically asking tech recruiters for compensation
information.

Built on:
* JMAP client: [jmapc][jmapc]
* Quoted email reply assembly: [replyowl][replyowl]

## Installation

[wafflesbot is available on PyPI][pypi]:

```
pip install wafflesbot
```

## Usage

wafflesbot provides the `waffles` command which can be run interactively or as a
cronjob.

Environment variables:
* `JMAP_HOST`: JMAP server hostname
* `JMAP_USER`: Email account username
* `JMAP_PASSWORD`: Email account password (likely an app password if 2-factor
authentication is enabled with your provider)

Required arguments:
* `-m/--mailbox`: Name of the folder to process
* `-r/--reply-content`: Path to file with an HTML reply message

### Invocation examples

Reply to messages in the "Recruiters" folder with the message in `my-reply.html`:
```py
JMAP_HOST=jmap.example.com \
JMAP_USER=ness \
JMAP_PASSWORD=pk_fire \
waffles \
--mailbox "Recruiters" \
--reply-content my-reply.html
```

Additional argument examples:

* Only reply to messages received within the last day:
* `waffles -m "Recruiters" -r my-reply.html --days 1` (or `-n`)
* Send at most 2 emails before exiting:
* `waffles -m "Recruiters" -r my-reply.html --limit 2` (or `-l`)
* Instead of sending mail, print constructed email replies to standard output:
* `waffles -m "Recruiters" -r my-reply.html --dry-run` (or `-p`)
* Log JMAP requests and responses to the debug logger:
* `waffles -m "Recruiters" -r my-reply.html --debug` (or `-d`)

## Development

Expand All @@ -15,8 +75,14 @@ Prerequisites: [Poetry][poetry]
Created from [smkent/cookie-python][cookie-python] using
[cookiecutter][cookiecutter]

[codecov]: https://codecov.io/gh/smkent/waffles
[cookie-python]: https://github.com/smkent/cookie-python
[cookiecutter]: https://github.com/cookiecutter/cookiecutter
[fastmail]: https://fastmail.com
[gh-actions]: https://github.com/smkent/waffles/actions?query=branch%3Amaster
[jmap]: https://jmap.io
[jmapc]: https://github.com/smkent/jmapc
[poetry]: https://python-poetry.org/docs/#installation
[pypi]: https://pypi.org/project/wafflesbot/
[replyowl]: https://github.com/smkent/replyowl
[repo]: https://github.com/smkent/waffles
17 changes: 9 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "waffles"
name = "wafflesbot"
version = "0.0.0"
description = "Tech recruiter auto reply bot using JMAP"
license = "GPL-3.0-or-later"
authors = ["Stephen Kent <smkent@smkent.net>"]
readme = "README.md"
repository = "https://github.com/smkent/waffles"
classifiers = [
"Development Status :: 1 - Planning",
"Operating System :: OS Independent",
Expand Down Expand Up @@ -37,7 +38,7 @@ freezegun = "^1.1.0"
types-freezegun = "^1.1.6"

[tool.poetry.scripts]
waffles = "waffles.main:main"
wafflesbot = "wafflesbot.main:main"

[tool.poetry-dynamic-versioning]
enable = true
Expand All @@ -48,18 +49,18 @@ style = "semver"
lt = ["lint", "test"]

lint = ["isort_lint", "black_lint"]
black_lint = { cmd = "black -l 79 -- tests/ waffles/" }
isort_lint = { cmd = "isort -- tests/ waffles/" }
black_lint = { cmd = "black -l 79 -- tests/ wafflesbot/" }
isort_lint = { cmd = "isort -- tests/ wafflesbot/" }

test = ["flake8", "isort", "black", "mypy", "pytest"]
black = { cmd = "black -l 79 --check --diff --color -- tests/ waffles/" }
isort = { cmd = "isort --check-only -- tests/ waffles/" }
black = { cmd = "black -l 79 --check --diff --color -- tests/ wafflesbot/" }
isort = { cmd = "isort --check-only -- tests/ wafflesbot/" }
flake8 = { cmd = "flake8" }
mypy = { cmd = "mypy" }
pytest = { cmd = "pytest" }

[tool.coverage.run]
source = ["waffles"]
source = ["wafflesbot"]

[tool.coverage.report]
fail_under = 0
Expand All @@ -71,7 +72,7 @@ profile = "black"
line_length = 79

[tool.mypy]
files = [ "tests", "waffles" ]
files = [ "tests", "wafflesbot" ]
disallow_untyped_defs = true
no_implicit_optional = true
check_untyped_defs = true
Expand Down
4 changes: 2 additions & 2 deletions tests/method_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ def make_email_send_call() -> mock._Call:
EmailHeader(
name="User-Agent",
value=(
"waffles/0.0.0 "
"wafflesbot/0.0.0 "
f"(jmapc {jmapc_version}, "
f"replyowl {replyowl_version})"
),
Expand All @@ -271,7 +271,7 @@ def make_email_send_call() -> mock._Call:
message_id=[
(
"1994.08.24T12.01.02"
"@waffles.dev.example"
"@wafflesbot.dev.example"
"_ness.onett.example.com"
)
],
Expand Down
2 changes: 1 addition & 1 deletion tests/test_jmap.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import jmapc

from waffles.jmap import JMAPClientWrapper
from wafflesbot.jmap import JMAPClientWrapper


def test_client() -> None:
Expand Down
4 changes: 2 additions & 2 deletions tests/test_module.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
def test_import() -> None:
import waffles
import wafflesbot

assert waffles
assert wafflesbot
29 changes: 14 additions & 15 deletions tests/test_waffles.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from freezegun import freeze_time
from jmapc.methods import IdentityGet

from waffles import Waffles
from wafflesbot import Waffles

from .method_utils import (
make_email_archive_call,
Expand All @@ -21,33 +21,32 @@
make_thread_search_response,
)

REPLY_TEMPLATE = (
"<b>Hi there</b>. I'm a <i>test</i> message for unit testing.<br />"
)


@pytest.fixture
def waffles() -> Iterable[Waffles]:
def wafflesbot() -> Iterable[Waffles]:
with freeze_time("1994-08-24 12:01:02"):
yield Waffles(
host="jmap-example.localhost",
user="ness",
password="pk_fire",
reply_template=REPLY_TEMPLATE,
reply_content=(
"<b>Hi there</b>. I'm a <i>test</i> message "
"for unit testing.<br />"
),
newer_than_days=7,
)


@pytest.fixture
def mock_methods(waffles: Waffles) -> Iterable[mock.MagicMock]:
def mock_methods(wafflesbot: Waffles) -> Iterable[mock.MagicMock]:
methods_mock = mock.MagicMock()
session_mock = mock.MagicMock(primary_accounts=dict(mail="u1138"))
with mock.patch.object(
waffles.client, "_session", session_mock
wafflesbot.client, "_session", session_mock
), mock.patch.object(
waffles.client, "method_call", methods_mock
wafflesbot.client, "method_call", methods_mock
), mock.patch.object(
waffles.client, "method_calls", methods_mock
wafflesbot.client, "method_calls", methods_mock
):
yield methods_mock

Expand Down Expand Up @@ -80,14 +79,14 @@ def assert_or_debug_calls(
@pytest.mark.parametrize(
"original_email_in_inbox", [True, False], ids=["in_inbox", "archived"]
)
def test_waffles(
waffles: Waffles,
def test_wafflesbot(
wafflesbot: Waffles,
mock_methods: mock.MagicMock,
dry_run: bool,
original_email_read: bool,
original_email_in_inbox: bool,
) -> None:
waffles.client.live_mode = not dry_run
wafflesbot.client.live_mode = not dry_run
expected_calls: List[mock._Call] = []
expected_calls.append(make_mailbox_get_call("pigeonhole"))
expected_calls.append(make_thread_search_call())
Expand Down Expand Up @@ -127,7 +126,7 @@ def test_waffles(
mock_responses.append(archive_response)
mock_methods.side_effect = mock_responses

waffles.process_mailbox("pigeonhole", limit=1)
wafflesbot.process_mailbox("pigeonhole", limit=1)
assert_or_debug_calls(mock_methods.call_args_list, expected_calls)
with pytest.raises(StopIteration):
mock_methods()
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion waffles/jmap.py → wafflesbot/jmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def _get_reply_address(self, email: Email) -> str:
def _make_messageid(self, mail_from: str) -> str:
dt = datetime.utcnow().isoformat().replace(":", ".").replace("-", ".")
dotaddr = re.sub(r"\W", ".", mail_from)
return f"{dt}@waffles.dev.example_{dotaddr}"
return f"{dt}@wafflesbot.dev.example_{dotaddr}"

def send_reply_to_email(
self,
Expand Down
8 changes: 4 additions & 4 deletions waffles/main.py → wafflesbot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument(
"-r",
"--reply-template",
dest="reply_template",
"--reply-content",
dest="reply_content",
metavar="file",
required=True,
type=argparse.FileType("r"),
help="Email reply template",
help="File with email reply HTML content",
)
ap.add_argument(
"-d",
Expand Down Expand Up @@ -74,7 +74,7 @@ def main() -> None:
user=os.environ["JMAP_USER"],
password=os.environ["JMAP_PASSWORD"],
live_mode=not args.dry_run,
reply_template=args.reply_template.read(),
reply_content=args.reply_content.read(),
newer_than_days=args.newer_than_days,
)
w.process_mailbox(args.mailbox, limit=args.limit)
File renamed without changes.
8 changes: 4 additions & 4 deletions waffles/waffles.py → wafflesbot/waffles.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ class Waffles:
def __init__(
self,
*args: Any,
reply_template: str,
reply_content: str,
newer_than_days: int = 1,
debug: bool = False,
**kwargs: Any,
):
self.client = JMAPClientWrapper(*args, **kwargs)
self.replyowl = ReplyOwl()
self.reply_template = reply_template
self.reply_content = reply_content
self.newer_than_days = newer_than_days
logging.basicConfig(level=logging.INFO)
log.setLevel(logging.DEBUG if debug else logging.INFO)
Expand Down Expand Up @@ -63,14 +63,14 @@ def _get_email_body_html(self, email: Email) -> Optional[str]:

def _reply_to_email(self, email: Email) -> None:
text_body, html_body = self.replyowl.compose_reply(
content=self.reply_template,
content=self.reply_content,
quote_html=self._get_email_body_html(email),
quote_text=self._get_email_body_text(email),
quote_attribution=self._quote_attribution_line(email),
)
assert text_body
user_agent = (
f"waffles/{version} ("
f"wafflesbot/{version} ("
+ ", ".join(
(
f"jmapc {jmapc_version}",
Expand Down

0 comments on commit aa92885

Please sign in to comment.