Skip to content

Commit 4368062

Browse files
MelissaAutumndavinotdavid
authored andcommitted
Add Stalwart 0.16 JMAP admin compatibility (#628, #1168) (#1131)
* Split up MailClient from clients.py into 3 different MailClient (incl interface.) * Some initial jmap work Not happy with the current state, but also not happy with existing jmap clients. The idea is to make this pretty lean but still allow for some developer ux wins. * Kick-off stalwart types, and start with get domain * WIP integration of pydantic * Fix jmap calls with, and add get_account * Add a few more types, wrangle `@type`, and add a few more methods to stalwart client jmap. * Base types off of BaseSchema and adjust properties to snake_case because python * Finish up create_account * Refactor clients test structure * Fix create_account's return, quota param, and deprecate app password. (It's not used.) * Update docblock for mail_client_jmap.py * Cache primary domain id, and fix get_account / add delete_account * * Move types into `mail.types.*` instead of being *_types.py inside client. * Rename AccountType/DomainType to just Account/Domain. * Include test script * * Add update_individual * Port jmap types to pydantic * Add save_app_password * Refactor how the jmap classes are setup so we don't confused admin calls with user calls. * Move domain lookup to its own function and replace the instance in create_account * WIP refactor to split up basic jmap ops * Refactor jmap clients, and fix up save_email_addresses * Refactor invocation names to the constants * Implement replace/delete email addresses, fix delete account, and swap exclude_none to exclude_unset * Add StorageQuota type, and implement update_quota * Refactor and add DkimSignature type * Move JMapType and Id into jmap types, and add get_identity with types. * Create private key and implement create_dkim for dkim1 signatures * Remove key dumper test code * Add delete_dkim * Implement most of the dkim/dns functions, and modify some return types. * Re-add function I accidentally cut * Merge stalwart v0.16 into main docker file so I can start running tests. Config is stored inside sqlite file, hopefully this is cross-compat... * Swap listener to 8180 for http * Add some minor hacks to get the dashboard working * Get domain verification working-ish for migrated users * Move main.db to main.db.example * Move config.json to config.json.example * Only volume mount the db file not the folder Otherwise we'll run into "attempt to write a readonly database" error. * Update bootstrap.py for v0.16 * Lint * Format * Fix import * Foce test_legacy to use mailclient legacy * `chmod 777 main.db` which gets around the cannot read readonly db error. * Hook-up new stalwart url, user, pass, and auth method * Add response dump files, an initial test and blank out debug dump * Lint / Format * Remove unused import * Fix ruff format checks --------- Co-authored-by: Davi Nakano <davinakanoca@gmail.com>
1 parent 8548a62 commit 4368062

51 files changed

Lines changed: 3240 additions & 59 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ STALWART_API_AUTH_METHOD=basic
9898
# Enable after Stalwart 0.16 upgrade
9999
STALWART_DKIM_STAGE_MANAGEMENT_ENABLED=false
100100

101+
# Use the v0.16 jmap api instead of the pre-v0.16 rest based api
102+
STALWART_ADMIN_API_USE_JMAP=false
103+
STALWART_JMAP_API_URL=http://stalwart_new:8080
104+
STALWART_JMAP_API_AUTH_USER=admin
105+
STALWART_JMAP_API_AUTH_SECRET=admin
106+
STALWART_JMAP_API_AUTH_METHOD=basic
107+
101108
# Hosted DKIM DNS delegation. Customers publish CNAMEs under their own domains
102109
# to these hosted records so Thunderbird can rotate DKIM keys later.
103110
HOSTED_DKIM_DOMAIN=dkim.thunderhosted.com

.env.test

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ STALWART_BASE_API_URL=http://stalwart:8080
8787
STALWART_API_KEY=this-is-mocked
8888
STALWART_API_AUTH_METHOD=basic
8989

90+
# Use the v0.16 jmap api instead of the pre-v0.16 rest based api
91+
STALWART_ADMIN_API_USE_JMAP=false
92+
STALWART_JMAP_API_URL=http://stalwart_new:8080
93+
STALWART_JMAP_API_AUTH_USER=admin
94+
STALWART_JMAP_API_AUTH_SECRET=admin
95+
STALWART_JMAP_API_AUTH_METHOD=basic
96+
9097
# Keycloak's (or another oidc provider) client settings
9198
OIDC_CLIENT_ID=tb-accounts
9299
OIDC_CLIENT_SECRET=

bootstrap.py

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from pathlib import Path
12
import argparse
23
import os
34
import shutil
@@ -10,35 +11,70 @@
1011
action='store_true',
1112
help='deletes any existing configs. WARNING: Make sure to backup your local env, and stalwart configs!',
1213
)
14+
arg_parse.add_argument(
15+
'--v016-only',
16+
action='store_true',
17+
help='Only bootstraps Stalwart v0.16 / Stalwart New',
18+
)
1319

1420
args = arg_parse.parse_args()
1521

1622

17-
def main():
18-
from_scratch = args.from_scratch
19-
20-
print('Bootstrapping project:') # noqa: T201
23+
def bootstrap_legacy(from_scratch):
2124
if not os.path.isfile('.env') or from_scratch:
2225
try:
2326
shutil.copy('.env.example', '.env')
2427
print('\t* Copied .env.example to .env') # noqa: T201
2528
except SameFileError:
2629
pass
2730

28-
if os.path.isdir('mail/data') and from_scratch:
29-
shutil.rmtree('mail/data')
30-
print('\t* Removed mail/data') # noqa: T201
31+
if os.path.isdir('mail/stalwart_legacy/data') and from_scratch:
32+
shutil.rmtree('mail/stalwart_legacy/data')
33+
print('\t* Removed mail/stalwart_legacy/data') # noqa: T201
3134

32-
if not os.path.isfile('mail/etc/config.toml') or from_scratch:
35+
if not os.path.isfile('mail/stalwart_legacy/etc/config.toml') or from_scratch:
3336
try:
34-
os.makedirs('mail/etc', exist_ok=True)
35-
shutil.copy('config.toml.example', 'mail/etc/config.toml')
36-
print('\t* Copied config.toml.example to mail/etc/config.toml') # noqa: T201
37+
os.makedirs('mail/stalwart_legacy/etc/', exist_ok=True)
38+
shutil.copy('config.toml.example', 'mail/stalwart_legacy/etc/config.toml')
39+
print('\t* Copied config.toml.example to mail/stalwart_legacy/etc/config.toml') # noqa: T201
3740
except SameFileError:
3841
pass
3942

43+
44+
def bootstrap_new(from_scratch):
45+
# Handle v0.16
46+
path = Path('mail/stalwart/lib')
47+
if os.path.isdir(path) and from_scratch:
48+
os.remove(path / 'main.db')
49+
print(f'\t* Removed {path}/main.db') # noqa: T201
50+
if not os.path.isfile(path / 'main.db'):
51+
shutil.copy(path / 'main.db.example', path / 'main.db')
52+
print(f'\t* Copied {path}/main.db.example to {path}/main.db') # noqa: T201
53+
54+
path = Path('mail/stalwart/etc')
55+
if os.path.isdir(path) and from_scratch:
56+
os.remove(path / 'config.json')
57+
print(f'\t* Removed {path}/config.json') # noqa: T201
58+
if not os.path.isfile(path / 'config.json'):
59+
shutil.copy(path / 'config.json.example', path / 'config.json')
60+
print(f'\t* Copied {path}/config.json.example to {path}/config.json') # noqa: T201
61+
4062
print('Finished!') # noqa: T201
4163

4264

65+
def main():
66+
new_only = args.v016_only
67+
from_scratch = args.from_scratch
68+
69+
print('Bootstrapping project:', args.__dict__) # noqa: T201
70+
71+
if not new_only:
72+
print('Bootstrapping v0.15') # noqa: T201
73+
bootstrap_legacy(from_scratch)
74+
75+
print('Bootstrapping v0.16') # noqa: T201
76+
bootstrap_new(from_scratch)
77+
78+
4379
if __name__ == '__main__':
4480
main()

docker-compose.yml

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,13 @@ services:
117117
- "./templates:/app/templates:z"
118118
- "./keycloak/themes:/app/keycloak/themes:z"
119119

120-
stalwart:
120+
stalwart_legacy:
121121
image: stalwartlabs/stalwart:v0.15.4
122122
#build:
123123
# context: ../stalwart
124124
# dockerfile: Dockerfile
125125
volumes:
126-
- "./mail:/opt/stalwart:z"
126+
- "./mail/stalwart_legacy:/opt/stalwart:z"
127127
ports:
128128
- "443:443"
129129
- "8080:8080"
@@ -141,6 +141,35 @@ services:
141141
ADMIN_SECRET: "{plain}accounts"
142142
MAIL_STALWART_PATH: "/opt/stalwart"
143143

144+
stalwart_new:
145+
image: stalwartlabs/stalwart:v0.16
146+
hostname: 'stalwart_new'
147+
domainname: 'stalwart_new.local'
148+
#entrypoint: bash
149+
volumes:
150+
- "./mail/stalwart/etc:/etc/stalwart:Z"
151+
- "./mail/stalwart/lib/main.db:/var/lib/stalwart/main.db:Z"
152+
ports:
153+
- "8443:443"
154+
- "8180:8080"
155+
# - "8081:8081"
156+
# - "25:25"
157+
# - "587:587"
158+
# - "465:465"
159+
# - "143:143"
160+
# - "993:993"
161+
# - "4190:4190"
162+
# - "110:110"
163+
# - "995:995"
164+
environment:
165+
STALWART_RECOVERY_ADMIN: "admin:admin"
166+
# admin@example.com : iKeBFzrCbRRzaAZW
167+
networks:
168+
default:
169+
aliases:
170+
- stalwart_new.local
171+
172+
144173
keycloak:
145174
#image: quay.io/keycloak/keycloak:latest
146175
build:

jmap_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""MailClientJMAP test script. These examples are tested against a stand-alone Stalwart v0.16 instance.
2+
The results and responses will be used in unit tests."""
3+
4+
from thunderbird_accounts.mail.clients.mail_client_jmap import MailClientAdminJMAP
5+
6+
jwt = None
7+
client = MailClientAdminJMAP()
8+
# user_client = MailClientUserJMAP(username='pizza666@example.org', user_jwt=jwt)
9+
10+
11+
# print('account->', client.get_account('admin@example.org'))
12+
# print('domain->', client.get_domain('example.com'))
13+
print('set account->', client.create_account([], 'pizza@example.org', 'PIZZA!', quota=15_000))
14+
print('delete account->', client.delete_account('pizza@example.org'))
15+
# print('save email->', client.save_email_addresses('admin@example.org', ['lols2@example.com']))
16+
# print('create domain->', client.create_domain('pizza.lol', 'weeee'))
17+
# print('delete domain->', client.delete_domain('pizza.lol'))
18+
# app_password = user_client.save_app_password('my cool device!!')
19+
# print('create app password->', app_password)
20+
# print('delete app password->', user_client.delete_app_password(app_password.id))
21+
# print('save alias->', client.save_email_addresses('admin@example.org', ['pants123@example.org']))
22+
# print('remove alias->', client.delete_email_addresses('admin@example.org', ['pants123@example.org']))
23+
# print('???', user_client.get_identity())
24+
# print('create dkim->',client.create_dkim('example.org'))
25+
26+
print('get dns record->', client._get_dns_records('example.org'))
27+
print('build dns record->', client.build_expected_dns_records('example.org'))
28+
print('check dns record->', client.check_domain_dns('faviconfetcher.ca'))
29+
30+
# print('->', AccountUpdate(aliases={'0': EmailAlias(enabled=True, name='beans', domain_id='g')}).model_dump())

mail/.gitignore

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
etc/config.toml
2-
data/*
3-
logs/*
1+
stalwart_legacy/data/*
2+
stalwart_legacy/logs/*
3+
stalwart/lib/main.db
4+
stalwart/etc/config.json
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"accountId": "d333333",
3+
"notDestroyed": {
4+
"d": {
5+
"type": "objectIsLinked",
6+
"objectId": {
7+
"object": "Domain",
8+
"id": "d"
9+
},
10+
"linkedObjects": [
11+
{
12+
"object": "DkimSignature",
13+
"id": "i0eilb9iaaqb"
14+
},
15+
{
16+
"object": "DkimSignature",
17+
"id": "i0eilcgcabab"
18+
}
19+
]
20+
}
21+
}
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"accountId": "d333333",
3+
"created": {
4+
"73f16c71-cb4b-42e4-85e7-ddb0f35a6444": {
5+
"id": "i3cjmrt2acac"
6+
}
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"accountId": "d333333",
3+
"created": {
4+
"5e30f478-0934-493a-8639-d515053f475e": {
5+
"id": "g"
6+
}
7+
}
8+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[
2+
"bv"
3+
]

0 commit comments

Comments
 (0)