Compare commits

...

3768 Commits

Author SHA1 Message Date
Carlos Quintana
a3bd6969ec
chore: extract delete custom domain from controller (#2229) 2024-09-20 14:19:28 +02:00
Adrià Casajús
38d377acb3
Extract contact creation logic to an external function (#2228)
* Extract contact creation logic to an external function

* PR comments
2024-09-20 10:11:57 +02:00
Carlos Quintana
d0ba7675f0
chore: event changes (#2227)
* chore: change max_retries to 10

* chore: only send custom domain deleted event if it is not a partner domain

* chore: newrelic events metric names rename

* chore: emit failed events metric

* chore: migration for contact.flags and custom_domain.pending_deletion

* chore: mark custom_domain as pending_deletion when deleting it

* chore: add event type to metric
2024-09-19 16:20:56 +02:00
Carlos Quintana
4d359cff7a
feat: allow to define partner records (#2226)
* refactor: make config variables explicit

* refactor: make mandatory to have -verification suffix for prefix
2024-09-18 12:18:30 +02:00
Carlos Quintana
b5866fa779
feat: allow to define partner records (#2225) 2024-09-18 12:12:42 +02:00
Carlos Quintana
f6708dd0b6
chore: refactor dns to improve testability (#2224)
* chore: refactor DNS client to its own class

* chore: adapt code calling DNS and add tests to it

* chore: refactor old dkim check not to clear flag
2024-09-17 16:15:10 +02:00
Carlos Quintana
065cc3db92
chore: refactor create custom domain (#2221)
* fix: scripts/new-migration to use poetry again

* chore: add migration to add custom_domain.partner_id

* chore: refactor create_custom_domain

* chore: allow to specify partner_id to custom_domain

* refactor: can_use_domain return cause

* refactor: remove intermediate result class
2024-09-17 10:30:55 +02:00
Carlos Quintana
647c569f99
feat: extract custom domain utils to a service (#2215) 2024-09-13 14:49:48 +02:00
Adrià Casajús
5301d2410d
Send alias creation time in alias status change event also and rename some proto fields (#2217) 2024-09-13 14:25:38 +02:00
Adrià Casajús
486b6a7ad1
Send the alias creation time in the alias creation event (#2216) 2024-09-13 13:39:41 +02:00
Adrià Casajús
025d4feba0
Sync on partner user creation + several fixes (#2214)
* Do not close session since it leads to orphan user object

* Redirect instead of render to avoid having to have a mailbox object

* On inital partner link/login trigger sync

* Update github action upload/artifact to v4

* Remove sys.exit used to test script locally

* Simplified script to update alias flags and note
2024-09-12 16:58:20 +02:00
Adrià Casajús
b61a171de3
Add tests that all events have all the info we need (#2211)
* Add tests that all events have all the info we need

* Reorder

* Fix import
2024-09-10 11:05:24 +02:00
Son Nguyen Kim
7856706da1
fix "authentication with proton" button in mobile view (#2210)
Co-authored-by: Son NK <son@simplelogin.io>
2024-09-09 11:22:33 +02:00
Adrià Casajús
1ba97eef6e
Fix: Add alias delete event to contain email 2024-09-06 18:23:12 +02:00
Adrià Casajús
9f33764068
Ensure mailbox verifcation exception is caught and show proper error to the user 2024-09-06 15:47:47 +02:00
Adrià Casajús
cc44247482
AdminPanel: Show up to 10 mailboxes found (#2204)
* AdminPanel: Show up to 10 mailboxes found

* Add links
2024-09-03 17:05:05 +02:00
Adrià Casajús
1fb2e8f01c
Fix newrelic test 2024-09-02 17:23:51 +02:00
Adrià Casajús
5b0fd3cee4
Email search improvements 2024-09-02 17:00:56 +02:00
Adrià Casajús
728f9bf1f8
Add metrics and logs for the event sending 2024-09-02 16:59:20 +02:00
Son Nguyen Kim
d49f6b88a9
Upgrade djlint and reformat all fiels (#2197)
* update djlint

* reformat all files

* update precommit version

---------

Co-authored-by: Son NK <son@simplelogin.io>
2024-08-28 13:07:34 +02:00
Son Nguyen Kim
c1625a8002
Fix user can't choose "not selected" for default alias domain (#2196)
* update contributing guide: replace rye by poetry and add a section for mac

* fix the bug where user can't choose "not selected" for the default alias domain

* ruff format

* remove trailing space

---------

Co-authored-by: Son NK <son@simplelogin.io>
2024-08-27 23:26:12 +02:00
Son Nguyen Kim
4b82dff070
Replace forum.simplelogin.io by github one (#2193)
Co-authored-by: Son NK <son@simplelogin.io>
2024-08-25 22:22:39 +02:00
Carlos Quintana
35a950da04
fix: add missing commits on event sending (#2192) 2024-08-23 13:32:32 +02:00
Carlos Quintana
737c561227
fix: specify default dispatcher in job runner (#2191) 2024-08-23 09:11:47 +02:00
Carlos Quintana
57991f4d6b
feat: add command to debug sync events (#2190) 2024-08-21 10:35:08 +00:00
Carlos Quintana
33c418d7c6
chore: allow to define a different DB_URI for event listener (#2189) 2024-08-20 14:01:48 +00:00
Carlos Quintana
a72b7bde92
chore: add config for enabling sync for specific users (#2184)
* chore: add config for enabling sync for users

* chore: error handling
2024-08-19 06:35:39 +00:00
Adrià Casajús
d5869b849c
Add show domain alias and deleted alias 2024-08-08 15:50:57 +02:00
Adrià Casajús
a8988cb8f6
Limit email search to only 10 aliases to avoid timing out (#2183) 2024-08-08 10:49:00 +00:00
Adrià Casajús
80d1369bf9
Update tests 2024-08-05 11:54:44 +02:00
Adrià Casajús
8dfa886024
Admin panel improvements (#2179) 2024-08-02 16:15:18 +00:00
Adrià Casajús
ab26dd3cb4
Fix missing test 2024-08-02 17:57:54 +02:00
Adrià Casajús
4c035ca340
Return mailbox activation on mailbox creation 2024-08-02 14:53:46 +02:00
Adrià Casajús
ea138070fd
Added test to create mailbox without sending an email 2024-08-02 14:32:30 +02:00
Adrià Casajús
b0849bff6d
Allow to skip sending the mailbox verification email when creating a mailbox 2024-08-02 14:28:18 +02:00
Adrià Casajús
9b2e8c2e44
Use different errors when handling mailboxes (#2178)
* Use different errors when handling mailboxes

* Update test
2024-08-02 07:19:27 +00:00
Carlos Quintana
b823f4359a
fix: do not log health requests (#2177) 2024-08-02 07:18:16 +00:00
Adrià Casajús
2478def834
Allow to create pre-verified mailboxes 2024-08-01 17:19:03 +02:00
ccb78
5b784e8989
remove “shady” words (#2176) 2024-08-01 13:35:48 +00:00
Carlos Quintana
429ebf57cf
chore: add health endpoint (#2175) 2024-08-01 07:12:12 +00:00
Adrià Casajús
7b44226317
Fix invalid import 2024-07-30 18:11:57 +02:00
Adrià Casajús
b80e56a988
Move set default mailbox to settings (#2173) 2024-07-30 18:00:24 +02:00
Adrià Casajús
6faec9ba4d
Enforce user match on mailbox verification and improve logging (#2172) 2024-07-30 15:43:32 +02:00
Adrià Casajús
d11c2686b9
Move mailbox management to a module (#2164) 2024-07-30 13:36:48 +02:00
Adrià Casajús
10cfc21fe9
Revert back to poetry (#2171) 2024-07-30 10:38:19 +02:00
Adrià Casajús
09d955e6ea
Update redis dependency 2024-07-30 09:52:24 +02:00
LamTrinh.Dev
daad62b6eb
Update README.md (#2167)
Enhance Markdown for highlight DISABLE_REGISTRATION and DISABLE_ONBOARDING param in simplelogin.env .
2024-07-29 19:44:01 +00:00
Adrià Casajús
02a0f7bf98
Fix hatchling packaging (#2169) 2024-07-29 14:49:06 +00:00
Adrià Casajús
08a64f0fa6
Force contraints location 2024-07-29 13:41:43 +02:00
Adrià Casajús
02b506ba0f
Fix positional args 2024-07-25 17:03:55 +02:00
Adrià Casajús
32488284ec
Update yacron 2024-07-25 16:46:20 +02:00
Adrià Casajús
127bb5b98c
Replace poetry with rye (#2163) 2024-07-25 16:18:49 +02:00
Adrià Casajús
574a916cff
Remove requred from positional args 2024-07-25 10:08:15 +02:00
Adrià Casajús
8262390bf0
Close sessions between loops to make sure we leave no lock (#2162)
* Close sessions between loops to make sure we leave no lock

* Close at the end

* Close before sleeps

* Use python generic empty list in case the events is an iterator
2024-07-24 14:49:55 +00:00
Adrià Casajús
666bf86441
Rename method to account for domain being a string and not an int (#2161) 2024-07-23 15:58:52 +00:00
Adrià Casajús
1407c969d2
Only allow latest activation code to be used (#2160)
(cherry picked from commit dd09297bead4ea27731ac3bd60fcf2a3e7001268)
2024-07-23 14:23:37 +00:00
Adrià Casajús
a7aec0c37a
Move set default domain for alias to an external function (#2158)
* Move set default alias to a separate method to reuse it

* Add tests

* Find domains by domain not by id

* Revert models and setting changes

* Remove non required function
2024-07-23 14:17:23 +00:00
Carlos Quintana
71ce0f6253
chore: add retry counter to event (#2159) 2024-07-23 14:11:16 +00:00
Adrià Casajús
25022b4ad8
Several fixes (#2157)
* Ensure uploaded pictures are images and delete the previous ones

* Add CSRF protection to admin routes

* Only allow https urls in the client envs

* Close connection to try to get a new one

* Missing parameter

* start_time can be non existant. Set a default value
2024-07-18 12:48:18 +00:00
Adrià Casajús
3afc90d3fb
Disable the enforced header until all extensions are updated and add a fallback option to trigger a manual login (#2155) 2024-07-12 15:27:11 +00:00
Adrià Casajús
1482bb4a33
Add to static js also the headers (#2153)
* Add to static js also the headers

* Move all header generation to a function
2024-07-11 12:28:22 +00:00
Adrià Casajús
e0d4ee9f8c
Set session to lax 2024-07-10 14:06:26 +02:00
Adrià Casajús
747dfc04bb
Fix base test class (#2152) 2024-07-10 11:41:50 +00:00
Adrià Casajús
d8f7cb2852
Use header in api tests 2024-07-10 13:14:42 +02:00
Adrià Casajús
5d48b5878f
Restrict cookie usage on api endpoints (#2151) 2024-07-10 10:48:46 +00:00
Carlos Quintana
cccd65d93a
fix: contact duplicate key (#2150) 2024-07-10 10:46:54 +00:00
Carlos Quintana
87e55605b8
fix: coinbase float user id (#2149) 2024-07-10 07:58:17 +00:00
Carlos Quintana
ae9f47d5a5
fix: remove unnecessary staticmethod (#2147) 2024-07-10 07:40:37 +00:00
Carlos Quintana
f05f01bf77
chore: QOL improvements on alias delete due to cascade FKs (#2144) 2024-07-08 14:39:18 +00:00
Adrià Casajús
2d841e9bc0
Update render function to receive user always as a param (#2141)
* Update render function to receive user always as a param

(cherry picked from commit fb53632298b08ab40bb82b8c8724a0bf254b2632)

* Add user to the kwargs
2024-07-03 12:59:16 +00:00
danfate
e71d6264a7
convert POSTFIX_TIMEOUT to int (#2135) 2024-07-02 12:24:50 +00:00
Adrià Casajús
24e211ac68
Add warning to subject when possible phishing is detected (#2137)
(cherry picked from commit 8f714b9fab49354bfcc10dad8e149a8a0aefdc4c)
(cherry picked from commit 21490ec1934b74de7d2e38326735329a87cf5dfd)
2024-07-01 16:43:48 +00:00
Adrià Casajús
faae37b6bc
Use partner emails when the user has used alias from a partner (#2136)
* Update base templates based on the parter user

* Update template

* Fix missing check

* Check if the user is set

* Hide flag usage
2024-06-28 13:34:16 +00:00
Ggcu
3fd9884c56
fix emails (#2111)
* Update trial-end.html

* Update trial-end.txt.jinja2

* Update subscription-end.txt

* Update subscription-end.html
2024-06-28 10:33:17 +00:00
ghisch
4817dfdcaf
[Security] Remediate 2FA bypass with hashed recovery code (#2132)
* Fix Vuln (allow 2FA bypass with hashed recovery code)

Remove comparison of hashed recovery code from db with the user input.

* Formatting

* Remove Comment
2024-06-26 16:26:46 +00:00
Adrià Casajús
1ecc5eb89b
Log when a partner user is unlinked (#2133) 2024-06-26 10:17:24 +00:00
Son Nguyen Kim
209ed65ebc
Disable pgp onboarding proton mail (#2122)
* show app page

* Do not send onboarding PGP email to Proton mailbox

---------

Co-authored-by: Son NK <son@simplelogin.io>
2024-06-10 11:58:04 +00:00
Adrià Casajús
8a77a8b251
Create jobs to trigger sending all alias as create events (#2126)
* Create jobs to trigger sending all alias as create events

* Set events in past tense

* fix test

* Removed debug log

* Log messages
2024-06-07 13:36:18 +00:00
Carlos Quintana
b931518620
Add create alias list event (#2125)
* chore: add alias create list proto event

* chore: generate python files from proto
2024-06-06 09:05:47 +00:00
Carlos Quintana
9d2a35b9c2
fix: monitoring table name (#2120) 2024-05-24 11:09:10 +02:00
Carlos Quintana
5f190d4b46
fix: monitoring table name 2024-05-24 10:52:08 +02:00
Carlos Quintana
6862ed3602
fix: event listener (#2119)
* fix: commit transaction after taking event

* feat: allow to reconnect to postgres for event listener

* chore: log sync events pending to process to metrics

* fix: make dead_letter runner able to process events without needing to have lock on the event

* chore: close Session after reconnect

* refactor: make EventSource emit only events that can be processed
2024-05-24 10:21:19 +02:00
Carlos Quintana
450322fff1
feat: allow to disable event-webhook (#2118) 2024-05-23 16:50:54 +02:00
Carlos Quintana
aad6f59e96
Improve error handling on event sink (#2117)
* chore: make event_sink return success

* fix: add return to ConsoleEventSink
2024-05-23 15:05:47 +02:00
Carlos Quintana
8eccb05e33
feat: implement HTTP event sink (#2116)
* feat: implement HTTP event sink

* Update events/event_sink.py

---------

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2024-05-23 11:32:45 +02:00
Carlos Quintana
3e0b7bb369
Add sync events (#2113)
* feat: add protocol buffers for events

* chore: add EventDispatcher

* chore: add WebhookEvent class

* chore: emit events

* feat: initial version of event listener

* chore: emit user plan change with new timestamp

* feat: emit metrics + add alias status to create event

* chore: add newrelic decorator to functions

* fix: event emitter fixes

* fix: take null end_time into account

* fix: avoid double-commits

* chore: move UserDeleted event to User.delete method

* db: add index to sync_event created_at and taken_time columns

* chore: add index to model
2024-05-23 10:27:08 +02:00
Son Nguyen Kim
60ab8c15ec
show app page (#2110)
Co-authored-by: Son NK <son@simplelogin.io>
2024-05-22 15:43:36 +02:00
Son Nguyen Kim
b5b167479f
Fix admin loop (#2103)
* mailbox page requires sudo

* fix the loop when non-admin user visits an admin URL

https://github.com/simple-login/app/issues/2101

---------

Co-authored-by: Son NK <son@simplelogin.io>
2024-05-10 18:52:12 +02:00
Adrià Casajús
8f12fabd81
Make hibp rate configurable (#2105) 2024-05-10 18:51:16 +02:00
Daniel Mühlbachler-Pietrzykowski
b6004f3336
feat: use oidc well-known url (#2077) 2024-05-02 16:17:10 +02:00
Adrià Casajús
80c8bc820b
Do not double count AlilasMailboxes with Aliases (#2095)
* Do not double count aliasmailboxes with aliases

* Keep Sl-Queue-id
2024-04-30 16:41:47 +02:00
Son Nguyen Kim
037bc9da36
mailbox page requires sudo (#2094)
Co-authored-by: Son NK <son@simplelogin.io>
2024-04-23 22:25:37 +02:00
Son Nguyen Kim
ee0be3688f
Data breach (#2093)
* add User.enable_data_breach_check column

* user can turn on/off the data breach check

* only run data breach check for user who enables it

* add tips to run tests using a local DB (without docker)

* refactor True check

* trim trailing space

* fix test

* Apply suggestions from code review

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>

* format

---------

Co-authored-by: Son NK <son@simplelogin.io>
Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2024-04-23 22:16:36 +02:00
Adrià Casajús
015036b499
Prevent proton mailboxes from enabling pgp encryption (#2086) 2024-04-12 15:19:41 +02:00
Son Nguyen Kim
d5df91aab6
Premium user can enable data breach monitoring (#2084)
* add User.enable_data_breach_check column

* user can turn on/off the data breach check

* only run data breach check for user who enables it

* add tips to run tests using a local DB (without docker)

* refactor True check

* trim trailing space

* fix test

* Apply suggestions from code review

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>

* format

---------

Co-authored-by: Son NK <son@simplelogin.io>
Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2024-04-12 10:39:23 +02:00
Adrià Casajús
2eb5feaa8f
Small improvements (#2082)
* Update logs with more relevant info for debugging purposes

* Improved logs for alias creation rate-limit

* Reduce sudo time to 120 secs

* log fixes

* Fix missing object to add to the session
2024-04-08 15:05:51 +02:00
Adrià Casajús
3c364da37d
Dmarc fix (#2079)
* Add log to spam check + remove invisible characters on import

* Update log
2024-03-26 11:43:33 +01:00
Adrià Casajús
36cf530ef8
Preserve X-SL-Queue-Id (#2076) 2024-03-22 11:00:06 +01:00
Adrià Casajús
0da1811311
Cleanup old data (#2066)
* Cleanup tasks

* Update

* Added tests

* Create cron job

* Delete old data cron

* Fix import

* import fix

* Added delete + script to disable pgp for proton mboxes
2024-03-18 16:00:21 +01:00
Adrià Casajús
f2fcaa6c60
Cleanup also messsage-id headers from linebreaks (#2067) 2024-03-18 14:27:38 +01:00
Adrià Casajús
aa2c676b5e
Only check HIBP alias of paid users (#2065) 2024-03-15 10:13:06 +01:00
Adrià Casajús
30ddd4c807
Update oneshot commands (#2064)
* Update oneshot commands

* fix

* Fix test_load command

* Rename to avoid test executing it

* Do HIBP breach check in batches instead of a single load
2024-03-14 16:03:43 +01:00
Son Nguyen Kim
f5babd9c81
Move import export back to setting (#2063)
* replace black by ruff

* move alias import/export to settings

* fix html closing tag

* add rate limit for alias import & export

---------

Co-authored-by: Son NK <son@simplelogin.io>
2024-03-14 15:56:35 +01:00
Adrià Casajús
74b811dd35
Update oneshot commands (#2060)
* Update oneshot commands

* fix

* Fix test_load command

* Rename to avoid test executing it
2024-03-14 11:11:50 +01:00
martadams89
e6c51bcf20
ci: remove v7 (#2062) 2024-03-14 09:49:45 +01:00
martadams89
4bfc6b9aca
ci: add arm docker images (#2056)
* ci: add arm docker images

* ci: remove armv6

* ci: update workflow to run only on path

* Update .github/workflows/main.yml

---------

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2024-03-13 15:20:47 +01:00
Adrià Casajús
e96de79665
Add missing indexes and mark aliases as created by partner (#2058)
* Add missing indexes and mark aliases as created by partner

* Configure if we should skip the partner aliases or not
2024-03-13 14:30:17 +01:00
Daniel Mühlbachler-Pietrzykowski
a608503df6
feat: add generic OIDC connect (#2046) 2024-03-13 14:30:00 +01:00
Son Nguyen Kim
0c3c6db2ab
point to the new safari extension (#2059)
Co-authored-by: Son NK <son@simplelogin.io>
2024-03-12 23:05:54 +01:00
Adrià Casajús
9719a36dab
Do not replace unsubs that go to UNSUBSCRIBER (#2051) 2024-03-06 16:26:10 +01:00
Adrià Casajús
a7d4bd15a7
If the transactional_id is None do nothing 2024-03-04 17:47:06 +01:00
Adrià Casajús
565f6dc142
If there is no transactional id, skip it (#2047) 2024-03-04 17:44:19 +01:00
Adrià Casajús
76423527dd
Update HIBP async script (#2043)
* Update HIBP async script

* Fix: continue instead of return
2024-03-04 13:12:38 +01:00
Adrià Casajús
501b225e40
Require sudo for account changes (#2041)
* Move accounts settings under sudo

* Fixed sudo mode

* Add a log message

* Update test

* Renamed sudo_setting to account_setting

* Moved simple login data export and alias/import export to account settings

* Move account settings to the top-right dropdown
2024-02-29 11:20:29 +01:00
Adrià Casajús
1dada1a4b5
Allow to skip creating transactional emails (#2042) 2024-02-27 16:52:45 +01:00
Adrià Casajús
37f227da42
Fix format 2024-02-27 09:41:47 +01:00
Adrià Casajús
97e68159c5
Fix: Never use NOREPLY to create contacts (#2039) 2024-02-27 09:29:53 +01:00
Adrià Casajús
673e19b287
Sanitize unused next parameter (#2040) 2024-02-26 19:23:03 +01:00
Sukuna
5959d40a00
Added comments to test_login.py (#2035)
* Added comments to test_login.py

-Added comments to each test function to provide clear documentation of the test steps.
-Comments detail the purpose of each test, the actions taken, and the expected outcomes.
-Improved readability and maintainability of the test suite.
-No changes in functionality; only added comments for better code understanding.

* Removed comments from import file in test_login.py
2024-02-26 17:41:54 +01:00
Adrià Casajús
173ae6a221
Allow to soft-delete users (#2034)
* Allow the possibility of soft-deleting users

* Unschedule for delete after link

* Add dry run to the cron
2024-02-22 17:38:34 +01:00
Adrià Casajús
eb92823ef8
Removed potentially nsfw words 2024-02-20 11:17:28 +01:00
Adrià Casajús
363b851f61
Fix: use proper bucket time for the rate limit 2024-02-20 11:13:06 +01:00
Adrià Casajús
d0a6b8ed79
Add start and end flags to parallelize call (#2033) 2024-02-19 16:46:35 +01:00
Adrià Casajús
50c130a3a3
Store the latest email_log id in the alias to simplify dashboard query (#2022)
* Store the latest email_log id in the alias to simplify dashboard query

* Fix test

* Add script to migrate users last email_log_id to alias

* Always update the alias last_email_log_id automatically

* Only set the alias_id if it is set

* Fix test with randomization

* Fix notification test

* Also remove explicit set on tests

* Rate limit alias creation to prevent abuse (#2021)

* Rate limit alias creation to prevent abuse

* Limit in secs

* Calculate bucket time

* fix exception

* Tune limits

* Move rate limit config to configuration (#2023)

* Fix dropdown item in header (#2024)

* Add option for admin to stop trial (#2026)

* Fix: if redis is not configured do not enable rate limit (#2027)

* support product IDs for the new Mac app (#2028)

Co-authored-by: Son NK <son@simplelogin.io>

* Add metrics to rate limit (#2029)

* Order domains alphabetically when retrieving them (#2030)

* Removed unused import

* Remove debug info

---------

Co-authored-by: D-Bao <49440133+D-Bao@users.noreply.github.com>
Co-authored-by: Son Nguyen Kim <son.nguyen@proton.ch>
Co-authored-by: Son NK <son@simplelogin.io>
2024-02-15 15:48:02 +01:00
Adrià Casajús
b462c256d3
Order domains alphabetically when retrieving them (#2030) 2024-02-08 15:36:06 +01:00
Adrià Casajús
f756b04ead
Add metrics to rate limit (#2029) 2024-02-06 11:55:45 +01:00
Son Nguyen Kim
05d18c23cc
support product IDs for the new Mac app (#2028)
Co-authored-by: Son NK <son@simplelogin.io>
2024-02-06 11:54:02 +01:00
Adrià Casajús
4a7c0293f8
Fix: if redis is not configured do not enable rate limit (#2027) 2024-02-05 14:53:01 +01:00
Adrià Casajús
30aaf118e7
Add option for admin to stop trial (#2026) 2024-02-05 13:47:39 +01:00
D-Bao
7b0d6dae1b
Fix dropdown item in header (#2024) 2024-02-02 10:23:05 +01:00
Adrià Casajús
b6f1cecee9
Move rate limit config to configuration (#2023) 2024-02-01 14:47:15 +01:00
Adrià Casajús
d12e776949
Rate limit alias creation to prevent abuse (#2021)
* Rate limit alias creation to prevent abuse

* Limit in secs

* Calculate bucket time

* fix exception

* Tune limits
2024-01-30 18:29:59 +01:00
Ed
b8dad2d657
Update README.md (#2011)
Updated README.md to prevent Nginx redirecting the browser to the local address of the machine
2024-01-26 10:30:16 +01:00
Son Nguyen Kim
860ce03f2a
fix footer spacing again (#2018)
Co-authored-by: Son NK <son@simplelogin.io>
2024-01-26 10:27:57 +01:00
Son Nguyen Kim
71bb7bc795
fix space issue on footer (#2017)
Co-authored-by: Son NK <son@simplelogin.io>
2024-01-23 14:57:58 +01:00
Adrià Casajús
761420ece9
Prevent mailboxes that have been disabled from being used again (#2016)
* Prevent mailboxes that have been disabled from being used again

* Improve test

* Get one user since it will be unique
2024-01-23 14:57:40 +01:00
Adrià Casajús
c3848862c3
Fix: limit the id sizes we generate and remove spaces after unidecode 2024-01-22 17:42:58 +01:00
Adrià Casajús
da09db3864
Do not allow free users to create reverse alias to reduce abuse (#2013)
* Do not allow free users to create reverse alias to reduce abuse

* Update format

* Move function under user

* Update tests
2024-01-16 14:51:01 +01:00
Adrià Casajús
44138e25a5
Fix: Dedup the list of mailboxes for an alias (#2010) 2024-01-16 14:50:39 +01:00
Adrià Casajús
b541ca4ceb
Fix typo in email (#2008) 2024-01-10 10:30:16 +01:00
Revi99
66c18e2f8e
small fixes (#2001)
* add forum mention

* add forum mention

* Add forum mention

* add forum mention

* fix my mistake

* fix
2024-01-08 21:40:52 +01:00
Son Nguyen Kim
4a046c5f6f
fix error when user logs out, go back to /dashboard and has the server error (#2003)
* fix error when user logs out, go back to /dashboard and has the server error

* reformat files. Not run ruff on migrations/ and .venv

---------

Co-authored-by: Son NK <son@simplelogin.io>
2024-01-05 14:30:07 +01:00
Ueri8
a731bf4435
Small fixes due to SL moving to Switzerland (#1999)
* Fix footer due to recent changes

* Fix forum mention
2024-01-04 12:07:59 +01:00
SecurityGuy
f3127dc857
Generate working DKIM keys by adding -traditional flag and update NGINX instructions to avoid breaking certbot (#1989)
* Update README.md

Add -traditional option to openssl genrsa to avoid Python DKIM library (dkimpy) error that prevents email from being sent:

dkim.asn1.ASN1FormatError: Unexpected tag (got 30, expecting 02)

Ref: https://bugs.launchpad.net/dkimpy/+bug/1708917

* Update NGINX instructions

Include warning to delete /etc/nginx/sites-enabled/default to avoid a conflict that breaks certbot.
2024-01-03 14:08:39 +01:00
Kelp8
d9d28d3c75
I don’t think we really need that (#1992) 2024-01-03 14:08:05 +01:00
Kelp8
bca6bfa617
Remove sensitive words (#1994) 2024-01-03 12:38:13 +01:00
Agent-XD
5d6a4963a0
Small fixes (#1991)
* remove proprietary mention

* Add forum mention

* Sync activation.txt with activation.html

* Add subdomain information

* Make info look better

* Fix wording
2024-01-03 12:35:42 +01:00
Kelp8
00737f68de
Minor wordings change (#1985)
* Wording changes

* Add information to avoid being put in SPAM

* Remove word repeating

* Add forum mention

* Add forum mention to header.html

* Add info to avoid person marking as SPAM
2024-01-02 13:20:48 +01:00
Joseph Demcher
9ae206ec77
correct alias version in api.md (#1981) 2024-01-02 12:35:56 +01:00
Agent-XD
9452b14e10
Remove sensitive words (#1983)
* Remove sensitive words from test_words.txt

* Remove sensitive from words.txt

* remove sensitive from words_alpha.txt

* Update test_words.txt
2024-01-02 12:33:27 +01:00
Son Nguyen Kim
7705fa1c9b
reduce rate limit on /v2/aliases endpoint (#1979)
Co-authored-by: Son NK <son@simplelogin.io>
2023-12-27 16:42:58 +01:00
Adrià Casajús
1dfb0e3356
Require CSRF check on custom alias creation (#1977) 2023-12-20 16:15:01 +01:00
Adrià Casajús
2a9c1c5658
Increase limit for the dashboard and do it by user 2023-12-19 17:27:55 +01:00
Carlos Quintana
dc39ab2de7
chore: remove verbose log (#1971) 2023-12-15 10:39:02 +01:00
Adrià Casajús
fe1c66268b
Allow to use another S3 provider (#1970) 2023-12-14 15:55:37 +01:00
Adrià Casajús
72041ee520
Show BF banner until end of promotion (#1953) 2023-11-30 11:48:55 +01:00
Adrià Casajús
f81f8ca032
Further limit the index endpoint (#1950) 2023-11-21 17:44:33 +01:00
Adrià Casajús
31896ff262
Replace black and flake8 with ruff (#1943) 2023-11-21 16:42:18 +01:00
Adrià Casajús
45575261dc
Rate limit index endpoint (#1948) 2023-11-21 14:42:24 +01:00
Adrià Casajús
627ad302d2
Creating account via partner also canonicalizes email (#1939) 2023-11-08 09:58:01 +01:00
Son NK
08862a35c3 fix image size 2023-11-07 14:33:46 +01:00
Son Nguyen Kim
75dd3cf925
admin can clone newsletter (#1938)
* admin can clone newsletter

- remove unique constraint on newsletter subject
- admin can clone newsletter

* update coupon image

---------

Co-authored-by: Son NK <son@simplelogin.io>
2023-11-07 14:16:03 +01:00
Son Nguyen Kim
a097e33abe
black friday 2023 (#1937)
Co-authored-by: Son NK <son@simplelogin.io>
2023-11-07 13:53:28 +01:00
Adrià Casajús
e5cc8b9628
Update dockerfile to account for new build changes in yacron (#1936) 2023-11-07 11:09:55 +01:00
Hulk667i
d149686296
remove sensitive words (#1935) 2023-11-07 10:44:35 +01:00
Adrià Casajús
babf4b058a
Remove potentially conflictive words (#1932) 2023-11-02 17:33:03 +01:00
UserBob6
eb8f8caeb8
fix https://github.com/simple-login/app/issues/1925 (#1926)
Remove sensitive words
2023-10-16 22:19:20 +02:00
UserBob6
70fc9c383a
Remove sensitive words from words.txt https://github.com/simple-login/app/issues/1905 (#1921) 2023-10-16 21:39:53 +02:00
Adrià Casajús
b68f074783
Add index on message_id for foreign key (#1906)
* Add index on message_id for foreign key

* Revert cron changes
2023-10-05 10:55:29 +02:00
Adrià Casajús
73a0addf27
Remove bad word from wordlist 2023-10-03 12:04:50 +02:00
Adrià Casajús
e6bcf81726
Delete old email_log entries in batches to avoid table lock (#1902)
* Delete old email_log entries in batches to avoid table lock

* Avoid nested join

* Commiting after the batch delete

* Added statement count print

* Rename var
2023-10-02 10:50:02 +02:00
Adrià Casajús
7600038813
Update dependencies (#1901)
* Update dependencies

* Update python version

* update workflow to use python 3.10

* Install OS deps
2023-09-29 17:26:40 +02:00
Adrià Casajús
c19b62b878
Add index on created_at for EmailLog (#1898)
(cherry picked from commit ea46ca0af5f6912d17cf7c656f00257cdee191d1)
2023-09-28 18:26:40 +02:00
Jack Wright
4fe79bdd42
Update dns.html to amend DKIM configuration instructions (#1884)
When I was configuring my subdomain-based alias, I was wondering why it would not verify, even after waiting a day. But after playing a bit of whack-a-mole with my DNS settings, the proposed changes worked for me.
2023-09-26 12:21:23 +02:00
Son Nguyen Kim
fd1744470b
allow BCC (#1894)
Co-authored-by: Son NK <son@simplelogin.io>
2023-09-26 10:00:33 +02:00
Adrià Casajús
989a577db6
Allow to get premium partner domains without premium sl domains (#1880)
* Allow to get premium partner domains without premium sl domains

* Set condition on domains
2023-09-13 18:12:47 +02:00
Adrià Casajús
373c30e53b
Schedule deletion of users (#1872)
* Accounts to be scheduled to be deleted cannot receive emails or login

* Create model and create migration for user

* Add test for the cron function

* Move logic to one place

* Use the class name to call the static delete method
2023-09-10 22:11:50 +02:00
Son Nguyen Kim
ff3dbdaad2
add proton.ch to the is_proton check (#1863)
Co-authored-by: Son NK <son@simplelogin.io>
2023-09-04 21:21:39 +02:00
Adrià Casajús
7ec7e06c2b
Move alias transfer util outside the views to make it importable (#1855) 2023-08-31 13:42:44 +02:00
Adrià Casajús
ef90423a35
Fix: Use proper error when linking external partner accounts 2023-08-30 13:49:47 +02:00
Adrià Casajús
c04f5102d6
Fix: Handle email headers as strings if the are Header type (#1850) 2023-08-29 12:37:26 +02:00
Son Nguyen Kim
5714403976
Can use generic subject without pgp (#1847)
* improve wording for hide my subject option

* can use generic subject on a non-pgp mailbox

---------

Co-authored-by: Son NK <son@simplelogin.io>
2023-08-24 22:47:31 +02:00
Carlos Quintana
40ff4604c8
fix: handle Proton account not validated case (#1842) 2023-08-18 15:59:46 +02:00
mlec
66d26a1193
fix(core): Open mailto: links in a new tab when the default email client is set to a web mail (#1721)
* fix(build): Update docker image of Node to v20

- Open "mailto:" links in a new tab if using browser

* feat(dockerfile): revert node to v10.17.0
2023-08-15 16:03:04 +02:00
D-Bao
9b1e4f73ca
Update pricing page text (#1843) 2023-08-15 15:58:15 +02:00
Son Nguyen Kim
0435c745fd
disable the PGP section if the mailbox is proton and not has PGP enabled (#1841)
* disable the PGP section if the mailbox is proton and not has PGP enabled

* fix format

---------

Co-authored-by: Son NK <son@simplelogin.io>
2023-08-09 09:56:53 +02:00
Adrià Casajús
366631ee93
Limit length of contact names (#1837)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-08-04 16:17:45 +02:00
Adrià Casajús
4bf925fe6f
Revert contact creation (#1836)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-08-04 14:01:21 +02:00
Carlos Quintana
0e82801512
chore: add upcloud monitoring (#1835)
* chore: add upcloud monitoring

* Added db_role to new_relic metrics

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-08-04 12:19:00 +02:00
Adrià Casajús
9ab3695d36
Fix: Do not lowercase by default contact emails (#1834)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-08-04 10:36:13 +02:00
Son Nguyen Kim
06b7e05e61
raise exception when signature is empty (#1833)
Co-authored-by: Son NK <son@simplelogin.io>
2023-08-03 16:59:36 +02:00
Son Nguyen Kim
6c7e9e69dc
add logging in case of empty signature (#1832)
Co-authored-by: Son NK <son@simplelogin.io>
2023-08-03 10:22:02 +02:00
Adrià Casajús
6e4f6fe540
Sanitize alias, contacts, mailboxes and users before creating them (#1829)
* Sanitize alias, contacts, mailboxes and users before creating them

* Updated comments and moved crons to run when load is low

* Run the stats at the same time as previously

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-08-03 10:20:25 +02:00
Adrià Casajús
f2dad4c28c
Cron improvements (#1826)
* Yield on big queries and check the trial is active in the query directly

* Eagerly load the hibp aliases to check

* Updated trial condition

* Also yield referral

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-07-31 15:14:13 +02:00
Adrià Casajús
e9e863807c
Add missing indexes (#1824)
* Rate limit the sudo route

* Add missing indexes

* Updated index

* Update index creation to run with concurrent

* With autocommit block

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-07-29 10:03:31 +02:00
Adrià Casajús
c4003b07ac
Rate limit the sudo route (#1823)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-07-26 12:56:06 +02:00
Adrià Casajús
d8943cf126
Fix: Allow to create more than one api key if the user has more than one (#1822)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-07-25 17:15:18 +02:00
Maxime Labelle
2eec918543
Documented CAA (#1804)
* Documented CAA

* Fixed bold typo

* Clarified CAA configuration

* Highlighted bash syntax
2023-07-24 21:51:17 +02:00
Maxime Labelle
4d9b8f9a4b
Documented MTA-STS and TLSRPT (#1806) 2023-07-20 18:26:06 +02:00
Efren
81d5ef0783
Fix typo in placeholder text of form on support page (#1808)
"are" was missing an e
2023-07-20 18:15:27 +02:00
Adrià Casajús
04d92b7f23
Fix: Use MIMEText for text contents (#1801)
* Fix: For badly formatted messages use MIMEText

* Fix: For badly formatted messages use MIMEText

* fix test

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-07-11 16:48:01 +02:00
Adrià Casajús
cb900ed057
Fix: For badly formatted messages use MIMEText (#1800)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-07-11 16:23:37 +02:00
Adrià Casajús
516072fd99
Fix: save retries to disk (#1799)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-07-11 10:59:24 +02:00
Son Nguyen Kim
2351330732
mention about proton mail during signup (#1796)
* mention about proton mail during signup

* format

* trim whitespaces

---------

Co-authored-by: Son NK <son@sons-macbook-air-2.home>
Co-authored-by: Son NK <son@Sons-MacBook-Air-2.local>
2023-07-10 14:41:52 +02:00
Adrià Casajús
e2dbf8d48d
Avoid sending long encoded subject to sentry (#1798)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-07-10 14:41:42 +02:00
Adrià Casajús
d62bff8e46
Add rate limit and maximum amount of api keys (#1788)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-29 17:21:00 +02:00
Adrià Casajús
fc205157a8
Preserve also contact name in Original-From (#1787)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-29 16:21:22 +02:00
Adrià Casajús
ac9d550069
Fix: delete_header has no return value (#1786)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-27 14:42:52 +02:00
Adrià Casajús
daec781ffc
Fix unsubscribe header manipulation (#1785)
* Added debug statements to find out unsubscribe issues

* Add List-Unsubscribe headers to preserve list

* Cleanup debug messages

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-27 11:18:44 +02:00
Son Nguyen Kim
501c625ddf
set default alias suffix to word (#1765)
Co-authored-by: Son NK <son@Sons-MacBook-Air-2.local>
2023-06-27 11:07:02 +02:00
Adrià Casajús
d3aae31d45
Preserve original from header in X-SimpleLogin-Original-From (#1784)
* Preserve original from in the headers

* Update the settings page

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-23 12:43:06 +02:00
Adrià Casajús
8512093bfc
Update dockerfile to use netcat-traditional (#1782)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-22 10:58:00 +02:00
Adrià Casajús
76b05e0d64
Preserve original sender and authentication results if the original email is preserved in the alias (#1780)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-22 10:40:32 +02:00
Adrià Casajús
40663358d8
Add Object.freeze to prevent proto injections (#1781)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-21 18:57:54 +02:00
Adrià Casajús
f046b2270c
Fix: send also mailbox email to verify so that mailbox changes are not allowed (#1777)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-21 18:56:22 +02:00
Adrià Casajús
03c67ead44
Do not show the default domain twice (#1772)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-21 18:56:03 +02:00
Adrià Casajús
37ffe4d5fe
Fix: Always include default domain in the list of domains (#1768)
* Fix: Always include default domain in the list of domains

* Add premium test

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-06 15:55:10 +02:00
Adrià Casajús
689ef3a579
Check if the domain has a deleted alias (#1764)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-01 17:33:58 +02:00
Adrià Casajús
495d544505
Only retry n times each message (#1759)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-01 10:59:02 +02:00
Adrià Casajús
a539428607
Fix: If default domain is premium for free users do not offer it as an option (#1763)
* Fix: If default domain is premium for free users do not offer it as an option

* Refactored into simpler logic

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-06-01 10:46:25 +02:00
Adrià Casajús
8c7e9f7fb3
Fix: only send subscription notification if there is a valid subscription (#1762)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-31 18:20:18 +02:00
Adrià Casajús
9d9e5fcab6
Fix: If the default domain is hidden do not return it (#1761)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-31 17:59:49 +02:00
Adrià Casajús
ff33392398
Fix: use incorrect model to access profile picture path (#1760)
(cherry picked from commit e875f1dd40fe726f6e83aaa833f65eb9e10f7e94)

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-31 17:21:30 +02:00
Adrià Casajús
85964f283e
Add timeout to any outbound connection (#1756)
* Add timeout to any outbound connection

* Change log message to error

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-31 14:19:43 +02:00
Carlos Quintana
d30183bbda
fix: remove user password from export user data (#1758) 2023-05-31 09:40:20 +02:00
Adrià Casajús
ed66c7306b
Fix typo (#1755)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-29 17:50:41 +02:00
Adrià Casajús
07bb658310
Show the default domain for creating aliases even if it's not requested by a partner (#1754)
* Show the default domain in the suffixes even if it's not allowed

* Simplify logic

* Reformat

* Simplified logic

* Remove unused function

* Added test to validate suffixes

* Ensure we catch prefixes in test

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-29 16:40:04 +02:00
Adrià Casajús
e43a2dd34d
Have subscription callback whenever a subscription changes (#1748)
* Have subscription callback whenever a subscription changes

* Fixed tests

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-26 15:33:55 +02:00
Adrià Casajús
3de83f2f05
Add toggle to check if a user is premium without the partner subscription (#1739)
* Add toggle to check if a user is premium without the partner subscription

* fix test

* Parter created users do not have a newsletter alias id

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-15 12:34:58 +02:00
Adrià Casajús
e4d4317988
Various fixes (#1733)
* Reset all password tokens on password reset

* Added csrf validation on email change request and validation

* Return the same wether is a valid email or not

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-10 15:31:30 +02:00
Adrià Casajús
da2cedd254
Update package-lock.json to fix build error (#1732)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-10 11:18:45 +02:00
Adrià Casajús
e343b27fa6
Update package-lock.json (#1728)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-09 18:10:13 +02:00
Adrià Casajús
6dfb6bb3e4
Revert "Add code verification for creating mailboxes (#1725)" (#1727)
This reverts commit a5e7da10dd.

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-09 18:04:04 +02:00
Adrià Casajús
a5e7da10dd
Add code verification for creating mailboxes (#1725)
* Add code verification for creating mailboxes

* Added validation checks

* Use exceptions

* Added delete to the mailbox utils

* Fix test

* Update package.lock

* Fix delete error

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-09 16:35:58 +02:00
Adrià Casajús
5ddbca05b2
Check users aren't using an alias as their link email address for partner links (#1724)
(cherry picked from commit 93e24cb4239b812d46f119a982edd12de2406802)

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-08 18:47:10 +02:00
Faisal Misle
6c33e0d986
documentation clarification (#1717) 2023-05-03 19:56:22 +02:00
Adrià Casajús
7cb7b48845
Ensure coupons are only used once (#1718)
* Ensure coupons are only used once

* Update test to handle redirect

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-05-03 16:18:46 +02:00
Son Nguyen Kim
6276ad4419
Stats endpoint (#1716)
* update some dependencies: newrelic, gevent

that isn't compatible with python 3.11 on mac

* update package-lock using npm 9.6.4 and node 20.0

* Add GET /api/stats

* update pytest

---------

Co-authored-by: Son Nguyen Kim <son@Sons-MacBook-Air-2.local>
2023-05-03 10:15:47 +02:00
Son Nguyen Kim
66c3a07c92
Update dep (#1715)
* update some dependencies: newrelic, gevent

that isn't compatible with python 3.11 on mac

* update package-lock using npm 9.6.4 and node 20.0

---------

Co-authored-by: Son Nguyen Kim <son@Sons-MacBook-Air-2.local>
2023-05-02 23:01:55 +02:00
D-Bao
23a4e46885
add option to show/hide stats in aliases page (#1697) 2023-04-22 21:16:03 +02:00
Adrià Casajús
52e6f5e2d2
Fix: Allow contacts created with a domain to be delivered even if the domain cannot be used any more for contact creation (#1704)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-21 21:07:58 +02:00
Son Nguyen Kim
59c189957f
fix the E501 check (#1702) 2023-04-20 12:43:43 +02:00
Adrià Casajús
bec8cb2292
Alias domain as contact domain (#1689)
* Use the alias domain for contacts

* Check there are not duplicate emails

* Check also in trash

* Use helper

* Set VERP for the forward phase to the contact domain

* Add pgp_fingerprint as index for contacts

* Removed check trash

* Only use reply domains for sl domains

* Configure via db wether the domain can be used as a reverse_domain

* Fix: typo

* reverse logic

* fix migration

* fix test

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
Co-authored-by: Son <nguyenkims@users.noreply.github.com>
2023-04-20 12:14:53 +02:00
Adrià Casajús
7f23533c64
Fix sever typo (#1701)
* Fix: typo

* Limit the name to 100 chars

* Fix migration

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-20 11:06:59 +02:00
Adrià Casajús
62fecf1190
Add end_at index to PartnerSubscription (#1696)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-15 20:49:59 +02:00
Adrià Casajús
9d8116e535
Add migrations to create indexes (#1694)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-14 19:10:21 +02:00
Adrià Casajús
796c0c5aa1
Add alias indexes in the tables that refer to alias to speedup the alias deletion process (#1693)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-14 19:08:52 +02:00
Adrià Casajús
5a56b46650
Add pgp_fingerprint as index for contacts (#1692)
(cherry picked from commit 350d246d32)

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-14 18:29:06 +02:00
D-Bao
e3ae9bc6d5
Replace save/update buttons with an auto save feature (#1685)
* replace save/update buttons with auto save feature

* minor css improvement
2023-04-11 22:52:44 +02:00
Son Nguyen Kim
ec666aee87
minor wording change (#1684) 2023-04-07 09:24:06 +02:00
D-Bao
2230e0b925
Redesign new pricing page (#1680)
* redesign new pricing page

* add FAQ section

* reformatting using djlint

* fix djlint formatting

* minor Indentation adjustment
2023-04-07 09:22:57 +02:00
Adrià Casajús
71fd5e2241
Reduce rate limit on password forgot route (#1683)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-06 15:55:37 +02:00
Adrià Casajús
97cbff5dc9
Fix: add missing alias options (#1682)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-06 12:42:18 +02:00
Adrià Casajús
b6f79ea3a6
Refactor alias options and add it to more methods (#1681)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-06 11:07:13 +02:00
Adrià Casajús
43b91cd197
Create Partner only domains (#1665)
* Add Partner only domains

* Add hidden domain to the test and revert to default domains after the tests

* Send what to show in each call

* Fix: Pass none instead of false

* Removed flag from partnerusr

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-04-04 15:21:51 +02:00
Son Nguyen Kim
03e5083d97
use {word1}_{word2}{digits} as random alias address instead of {word1}{word2}{digits} (#1673) 2023-04-04 08:46:29 +02:00
Son Nguyen Kim
1f9d784382
Use a shorter suffix in case of custom domain (#1670) 2023-03-28 22:33:28 +02:00
dependabot[bot]
c09b5bc526
Bump redis from 4.3.4 to 4.5.3 (#1668)
Bumps [redis](https://github.com/redis/redis-py) from 4.3.4 to 4.5.3.
- [Release notes](https://github.com/redis/redis-py/releases)
- [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/redis/redis-py/compare/v4.3.4...v4.5.3)

---
updated-dependencies:
- dependency-name: redis
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-28 14:37:15 +02:00
Son Nguyen Kim
eba4ee8c2c
remove unnecessary plausible calls (#1664) 2023-03-27 10:48:41 +02:00
D-Bao
1c65094da8
Fix drag and drop to upload PGP public key not working on Firefox and Chromium (but working on Safari) (#1658)
* Fix pgp file drag and drop only worked on Safari

* Minor UI improvement of pgp public key text area

* add dashed outline only during dragover event
2023-03-27 10:48:27 +02:00
Carlos Quintana
2a014f0e4b
chore: add example to domain detail with subdomain (#1663)
* chore: add example to domain detail with subdomain

* Update templates/dashboard/domain_detail/dns.html

---------

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2023-03-24 10:33:00 +01:00
Son Nguyen Kim
b081b6a16a
track "visit pricing" and "upgraded" event (#1662) 2023-03-23 21:11:05 +01:00
Son Nguyen Kim
66039c526b
use PreserveOriginal as default (#1652) 2023-03-22 15:47:40 +01:00
Adrià Casajús
f722cae8d6
Add multiple registration warning message (#1653)
* Add multiple registration warning message

* Add alert

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-03-22 15:35:33 +01:00
Son Nguyen Kim
b6286e3c1b
Fix recreate alias from trash (#1641)
* no need to check for a deleted alias that belongs to user domain

* fix config.SAVE_UNSENT_DIR not set
2023-03-17 15:39:59 +01:00
Son Nguyen Kim
26d5fd400c
change docs.simplelogin.to to https://simplelogin.io/docs/siwsl/app/ (#1640)
* change docs.simplelogin.to to https://simplelogin.io/docs/siwsl/app/

* fix url
2023-03-17 15:39:47 +01:00
Son Nguyen Kim
b470ab3396
reset transfer token (#1638) 2023-03-17 11:47:11 +01:00
Adrià Casajús
66388e72e0
Feat: Use only sfw words with a number suffix (#1625)
* Feat: Use only sfw words with a number suffix

* Updated also custom aliases to have a number suffix

* do not use _ as separator

* use _ as separator for words-based suffix

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
Co-authored-by: Son <nguyenkims@users.noreply.github.com>
2023-03-13 19:55:16 +01:00
Adrià Casajús
432fb3fcf7
Fix: Send different exception for users with an alias as email (#1630)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-03-13 14:57:00 +01:00
Adrià Casajús
44e0dd8635
Break using an alias as a mailbox loop in the email_handler.py (#1624)
* Do not allow to use email alias as account email when linking

* Add missing status

* Remove TODO

* Also break contact as email loop

* Better test names

* Allow a reverse alias to send an email to an alias

* Ident fix

* Removed invalid test

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-03-13 13:01:00 +01:00
Adrià Casajús
2ec1208eb7
Remove dangerous words (#1620)
* Remove invalid words from word generation list

* Remove more dangerous words

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-03-08 09:09:30 +00:00
Adrià Casajús
87efe6b059
Remove invalid words from word generation list (#1615)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-03-03 17:30:53 +01:00
guzlewski
6a60a4951e
Add sender do message body (#1609) 2023-02-28 18:58:54 +01:00
Carlos Quintana
b3ce5c8901
chore: add noopener noreferrer to every target _blank (#1608) 2023-02-27 13:15:25 +01:00
Adrià Casajús
3fcb37f246
Reformat base64 encoded messages to shorter lines (#1575)
* Reformat base64 encoded messages to shorter lines

* Remove storing debug versions

* Add  example test email

* Update linelength to 76

* Revert changes in pre-commit

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-02-21 15:28:06 +01:00
Son Nguyen Kim
62ba2844f3
add admin page for InvalidMailboxDomain (#1573)
* add admin page for InvalidMailboxDomain

* show creation and modification date for InvalidMailboxDomain
2023-02-15 10:38:18 +01:00
Carlos Quintana
9143a0f6bc
fix: ensure contact name fits within db limits (#1568) 2023-02-10 10:07:43 +01:00
Adrià Casajús
48ae859e1b
Fix: Set the smtp default port in config to allow connect to port 25 with TLS (#1564)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-02-06 16:53:10 +01:00
Adrià Casajús
0a197313ea
Fix: allow receive email from non-canonical sources (#1545)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-01-25 13:17:20 +01:00
Son Nguyen Kim
b487b01442
Fix format (#1554)
* after deleting an alias, user should stay on the same page

* fix email indentation
2023-01-25 13:16:29 +01:00
Son Nguyen Kim
170082e2c1
after deleting an alias, user should stay on the same page (#1546)
* after deleting an alias, user should stay on the same page

* Fix delete alias mlec (#1547)

* Specify how to create the certificates if they don't exist in readme (#1533)

* Remove id= from get 🩹

* Add flash message level 🩹

* Rename transfer_mailbox back to new_mailbox in the create-mailbox part 🩹

Co-authored-by: rubencm <rubencm@gmail.com>

* Fix delete alias mlec (#1552)

* Specify how to create the certificates if they don't exist in readme (#1533)

* Remove id= from get 🩹

* Add flash message level 🩹

* Rename transfer_mailbox back to new_mailbox in the create-mailbox part 🩹

* Linting files to pass test 🎨

Co-authored-by: rubencm <rubencm@gmail.com>

Co-authored-by: mlec <42201667+mlec1@users.noreply.github.com>
Co-authored-by: rubencm <rubencm@gmail.com>
2023-01-25 13:16:10 +01:00
rubencm
51916a8c8a
Specify how to create the certificates if they don't exist in readme (#1533) 2023-01-17 20:09:41 +01:00
Adrià Casajús
4f2b624cc7
Set the proper link for unsub newsletter (#1503)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-01-17 11:56:00 +01:00
Adrià Casajús
81eb56e213
Tranfer aliases to a new mailbox when deleting mailboxes (#1534)
* Set up npm clean install instead of npm install in order to keep the version of npm packages 🎨

* Add option to transfer the alias to a new mailbox when a mailbox is deleted

* Moved alias transfer to job

* Lint

* Update forms

* Revert dockerfile change

Co-authored-by: ewen <ewen.coppens@a1.digital>
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-01-17 11:55:34 +01:00
Adrià Casajús
650a74ac00
Fix: Use npm ci instead of install to prevent install different versions (#1543)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-01-17 09:57:38 +01:00
Adrian Schnell
e6cdabd46e
Update docu for /api/alias/random/new (#1515)
the param `mode` has to be passed in the query
2023-01-12 15:55:07 +01:00
Adrià Casajús
d874acfe2c
Fix: Add CSRF validation to api key management page (#1523)
* Fix: Add CSRF validation to api key management page

* Added csrf to subdomain creation

* Added CSRF to totp cancel

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-01-12 12:34:47 +01:00
Adrià Casajús
0ab53ad49a
Fix: Use timed signers to avoid leaving permanent links (#1524)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-01-12 12:34:14 +01:00
Adrià Casajús
92de307c75
Added parallel limiting to creating custom domains, directories, mailboxes and subdomains (#1525)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-01-11 22:08:52 +01:00
Adrià Casajús
38c93e7f85
Fix: typo in the message (#1522)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2023-01-11 22:05:31 +01:00
Carlos Quintana
f2a840016b
chore: allow redis to support rediss (#1526) 2023-01-11 16:25:35 +01:00
Son Nguyen Kim
54997a8978
Manual sub reminder (#1519)
* use support page to renew sub

* remove other payment options
2023-01-11 14:29:41 +01:00
Son Nguyen Kim
be6bc7088e
use SL.com instead of SL.co in the example (#1506)
* use SL.com instead of SL.co in the example

* reduce the admin page size to speed up loading

* Revert "reduce the admin page size to speed up loading"

This reverts commit d7550ab153.
2022-12-28 09:37:50 +01:00
Adrià Casajús
ca0cbd911f
Remove bad words from the word list (#1500)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-21 07:56:50 +01:00
Adrià Casajús
0284719dbb
Fix: Remove * from a param (typo) (#1498)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-19 11:45:04 +01:00
Adrià Casajús
9378b8a17d
Fix: Return email in the get_communication_email always and search for the alias when needed (#1497)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-19 09:23:53 +01:00
Adrià Casajús
3f84a63e6d
Extend validity of totp tokens for up to a minute. (#1494)
* Feat: Allow TOTP for up to one minute in the future and in the past

* Feat: Allow TOTP for up to one minute in the future and in the past

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-16 17:54:46 +01:00
Adrià Casajús
5e48d86efa
Canonicalize emails from google and proton before registering users (#1493)
* Revert "Revert "Use canonical email when registering users (#1458)" (#1474)"

This reverts commit c8ab1c747e.

* Only canonicalize gmail and proton

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-14 11:50:36 +01:00
Adrià Casajús
9dcf063337
Rate limit changing user settings (#1491)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-13 18:48:44 +01:00
Adrià Casajús
73c0429cad
Fix: Set oneclick link for unsubscribe of the newsletter for tx emails (#1465)
* Feat: Add unsub oneclick to the base transactional email template

* Format

* Removed unused

* Format

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-13 16:59:14 +01:00
Adrià Casajús
21e9fce3ba
Set the admin view to show 100 entries by default (#1490)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-13 16:58:27 +01:00
Adrià Casajús
c8ab1c747e
Revert "Use canonical email when registering users (#1458)" (#1474)
* Revert "Use canonical email when registering users (#1458)"

This reverts commit f728b0175a.

* missing chang

* typo
2022-12-08 10:57:46 +01:00
Adrià Casajús
8636659ca9
Update docs to the same port as the reset script + remove pre-commit pylint (#1464)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-02 17:31:31 +01:00
Adrià Casajús
7e360bcbd9
Fix: Add mising csrf validation for contact pgp key modification (#1463)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-02 15:13:38 +01:00
Adrià Casajús
327b672f24
Set the user name on creation to the original email (#1462)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-01 13:07:36 +01:00
Adrià Casajús
12b18dd8b1
Revert BlackFriday banners (#1461)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-01 09:25:24 +01:00
Adrià Casajús
0996378537
Revert "Keep the dirty email after registering (#1459)" (#1460)
This reverts commit 0664e3b80c.

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-12-01 09:19:15 +01:00
Adrià Casajús
0664e3b80c
Keep the dirty email after registering (#1459)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-30 18:38:48 +01:00
Adrià Casajús
f728b0175a
Use canonical email when registering users (#1458)
* Use canonical email for registration, check both when checking if user exists

* Fix test

* Set pagesize to 100

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-30 17:19:55 +01:00
Adrià Casajús
53ef99562c
Add unsub link to newsletters (#1455)
* Add unsub link to newsletters

* Remove debug statement

* Updated unsub link

* Update unsub style

* Format

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-28 15:08:46 +01:00
Adrià Casajús
363a9932f1
Allow sentry to fail (#1454)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-28 12:40:06 +01:00
Adrià Casajús
b6ec4a9ac7
Update github checkout actions to v3 (#1451)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-28 11:13:27 +01:00
Adrià Casajús
3c36f37a12
Feat: Add enable/disable options in the admin panel (#1450)
* Feat: Add enable/disable options in the admin panel

* Fix duplicate method

* Black format

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-28 10:39:18 +01:00
Adrià Casajús
478b1386cd
Updated checkout action to full checkout the repo (#1436)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-24 10:37:04 +01:00
Spitfireap
b849d1cfa7
Simpler csv export (#1383)
* Export alias in csv

* reformating

* template

* Improved contributing script and doc

* Updated test

* removed csv export from GDPR export archive

* added test for new route

* fix trailing space

* moved test to new utils file
2022-11-23 13:51:08 +01:00
Adrià Casajús
0fbe576c44
Fix: Also replace source mailbox to alias when replacing stuff in the reply phase (#1432)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-18 14:30:19 +01:00
Son Nguyen Kim
d2360d1a99
update black friday wording (#1430) 2022-11-16 15:49:22 +01:00
Son Nguyen Kim
420bc56fc8
fix test (#1429) 2022-11-16 13:58:30 +01:00
Son Nguyen Kim
b3e9232956
show black friday banner (#1428)
* show black friday banner

* djlint
2022-11-16 13:43:09 +01:00
Son Nguyen Kim
989358af34
Fix empty authorized address (#1423)
* not allow empty authorized address

* check authorized address before adding

* use github for flake8

* fix test
2022-11-15 16:04:31 +01:00
Son Nguyen Kim
390b96b991
remove the code which is never called (#1407)
* remove the code which is never called

* fix comment

* no need to run ci for python 3.9
2022-11-15 10:07:06 +01:00
Adrià Casajús
4661972f97
Fix: When re-sending emails if they trigger exceptions move out of failed dir (#1411)
* Fix: When re-sending emails if they trigger exceptions move out of failed dir

* Use proper timeout

* Lint

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-10 13:24:46 +01:00
Son Nguyen Kim
25743da161
Dmarc transactional (#1402)
* make sure transactional email use the same domain for header from and envelope from

* fix import
2022-11-04 14:22:28 +01:00
Adrià Casajús
5bbf6a2654
Fix: Only override postfix port when enabling TLS if the port is set to be 25 (#1401)
* Fix: Only override postfix port when enabling TLS if the port is set to be 25

* Add connection timeout

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-04 11:13:19 +01:00
Adrià Casajús
dace2b1233
Fix: Do not re-re-deliver unsent mails on failure to re-deliver (#1397)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-03 17:48:09 +01:00
Adrià Casajús
afe2de4167
Fix: Create crontab for all hosts (#1396)
* Fix: Create crontab for all hosts

* Typo

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-02 18:13:24 +01:00
Adrià Casajús
efc7760ecb
Use newer github actions to install and cache poetry (#1395)
* Use newer github actions to install and cache poetry

* Update setup-python action to v4

* Parallel execution

* Build depends on lint

* Added missing req deps

* Install in all

* Remove unused

* No need to lint on all python versions

* Remove matrix deps

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-02 17:59:36 +01:00
Adrià Casajús
90d60217a4
Feat: Re-deliver mails (#1394)
* Feat: Send undelivered emails

* Add cron job

* Added to the crontab

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-02 15:51:14 +01:00
Adrià Casajús
3bc976c322
Feature: Add app name to each db connection (#1393)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-11-02 15:41:48 +01:00
Son Nguyen Kim
36d1626972
Notify another mailbox about an email sent by a mailbox to a reverse alias (#1381)
* Notify another mailbox about an email sent by a mailbox to a reverse alias

* keep reverse alias in CC and To header

* use alias as From to hint that the email is sent from the alias

* keep original subject, improve wording

* only add DKIM if custom domain has DKIM enabled
2022-10-30 19:59:42 +01:00
Adrià Casajús
6d8fba0320
Added too many exceptions test (#1378)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-27 14:04:03 +02:00
Son Nguyen Kim
02f42821c5
fix 21004 error (#1380) 2022-10-27 14:03:11 +02:00
Adrià Casajús
a5056b3fcc
Fix: Use source ip if user is not authenticated (#1379)
* Fix: Use source ip if user is not authenticated

* Fix lint

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-27 13:37:45 +02:00
Adrià Casajús
f6463a5adc
Change: Do not sleep on exclusive zones (#1375)
* Change: Do not sleep on exclusive zones

* Update test

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-27 10:40:33 +02:00
Adrià Casajús
7f9ce5641f
Feat: Added parallel limiter to prevent sqlalchemy transaction issues (#1360)
* Feat: Added parallel limiter to prevent sqlalchemy transaction issues

* Remove logs

* Moved initialization to its own file

* Throw exception

* Added test

* Add redis to gh actions

* Added v6 to the name

* Removed debug prints

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-27 10:07:02 +02:00
Adrià Casajús
d324e2fa79
Fix: Add csrf verification to directory updates (#1358)
* Fix: Add csrf verification to directory updates

* Update templates/dashboard/directory.html

* Added csrf for delete account form

* Fix tests

* Added CSRF check for settings page

* Added csrf to batch import

* Added CSRF to alias dashboard and alias transfer

* Added csrf to contact manager

* Added csrf to mailbox

* Added csrf for mailbox detail

* Added csrf to domain detail

* Lint

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-27 10:04:47 +02:00
Son Nguyen Kim
2f769b38ad
Apple in app fix (#1369)
* error log if issue with apple sub

* use the right secret when polling apple sub
2022-10-25 19:45:53 +02:00
Son Nguyen Kim
87047b3250
use /p.outbound.js and /p/api/event on app.sl.io (#1366) 2022-10-24 18:18:22 +02:00
Adrià Casajús
300f8c959e
Fix: Add words.txt to local data (#1365)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-24 17:55:59 +02:00
Son Nguyen Kim
8c73ff3c16
plausible: use https://simplelogin.io/p.outbound.js (#1364) 2022-10-24 16:44:01 +02:00
Son Nguyen Kim
9b452641a8
rename analytics.js to an.js (#1363) 2022-10-24 15:47:02 +02:00
Son Nguyen Kim
35470613d3
add DailyMetric and Metric as admin page, remove EmailLog admin page (#1352) 2022-10-15 19:10:39 +02:00
Son Nguyen Kim
c71824c68e
Init daily metric (#1351)
* Add DailyMetric model

* increment nb_new_web_non_proton_user

* fix test

* fix test
2022-10-14 17:35:34 +02:00
Son Nguyen Kim
1fc75203f2
Improve test: disable rate limit during test and avoid conflicts between tests (#1349)
* disable rate limit during test, avoid conflict between tests

* fix test
2022-10-14 16:37:49 +02:00
Son Nguyen Kim
3a4dac15f0
Plausible roll up (#1350)
* enable plausible roll-up, use everything.simplelogin.com

* versionning analytics.js to avoid caching

* allow plausible custom event

* send "Complete registration" event when user finishes signup

* remove blank lines
2022-10-14 10:38:43 +02:00
Son Nguyen Kim
7b24cdd98a
Revert "remove deduct_limit as it has no effect (#1347)" (#1348)
This reverts commit 851ba0a99a.
2022-10-13 22:00:45 +02:00
Son Nguyen Kim
851ba0a99a
remove deduct_limit as it has no effect (#1347)
* remove deduct_limit as it has no effect

- disable rate limit during test
- randomize data in test
- support non-empty db in test

* fix more test
2022-10-13 18:55:22 +02:00
Son Nguyen Kim
3be75a1bd9
fix copy to clipboard (#1346) 2022-10-13 17:29:01 +02:00
Adrià Casajús
72277211bb
For unauthenticated sessions only store them in redis for 5m (#1345)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-13 15:55:08 +02:00
Adrià Casajús
d5ca316e41
Have custom domains set up multiple dkim records to be able to rotate keys (#1334)
* Have custom domains set up multiple dkim records to be able to rotate keys

* Apply suggestions from code review

* Some PR comments

* Keep dkim enabled if it is already

* Format

* PR updates

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-11 07:17:37 +02:00
Son Nguyen Kim
f3bfc6e6a1
djlint (#1342) 2022-10-10 10:25:53 +02:00
mfmw123
21ce5c8e10
Corrections & consistent footer (#1338)
* Corrections and consistent footer

- _Downloads_ instead of _Features_
- Made _open source_ a link
- Deleted _-_ in the _open source_
- Added comparisons to be consistent with the main page
- Fixed GitHub spelling

* fix styling

Co-authored-by: Son Nguyen Kim <nguyenkims@users.noreply.github.com>
2022-10-10 10:17:12 +02:00
Son Nguyen Kim
1c5a547cd0
do not quarantine an email if fails DMARC but has a small rspamd score (#1337)
* do not quarantine an email if fails DMARC but has a small rspamd score

* use 0 when cannot parse rspamd score

* use -1 as default value
2022-10-10 10:13:07 +02:00
Son Nguyen Kim
5088604bb8
Replace reverse alias (#1335)
* replace any reverse alias by real address for all contacts

* improve logging

* fix comment

* Request contacts in batches of 100 to avoid loading the db

* Fix typo

* Added tests for the contact replacement

* Increase batch size to 1k

* Revert and use only reply_email and website_email

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-10 10:00:19 +02:00
Son Nguyen Kim
4ff158950d
use Proton Mail instead of Protonmail (#1336) 2022-10-06 17:43:01 +02:00
Son Nguyen Kim
d159a51de4
update logo white (#1331) 2022-10-04 18:07:00 +02:00
Son Nguyen Kim
002897182e
use logo with Proton mention (#1330) 2022-10-04 11:14:23 +02:00
Adrià Casajús
faeddc365c
Display recovery codes for mfa only once (#1317)
* Recovery codes can only be shown after adding a 2FA code and cannot be seen afterwards

* Added recovery codes fix

* Updated models and script

* Formatting

* Format

* Added base code

* Updated wording

* Set the config by default

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-10-03 12:32:45 +02:00
Adrià Casajús
faaff7e9b9
Handle failed payments subscriptions in paddle (#1327)
* Handle failed payments subscriptions in paddle

* Added tests

* Remove unused import

* Remove unused import

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-09-30 17:51:06 +02:00
Son Nguyen Kim
d415974e3b
Handle undisclosed recipients header (#1314)
* remove TO header if it's set to "undisclosed-recipients:;"

more info on https://www.rfc-editor.org/rfc/rfc4356.txt

* remove unnecessary indentation character in plain text email
2022-09-27 09:43:58 +02:00
Carlos Quintana
fa50c23a43
Allow RedisSessionStore to connect to sentinel (#1307)
* Allow RedisSessionStore to connect to sentinel

* Reuse flask_limiter redis storage

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-09-23 10:23:07 +02:00
Son Nguyen Kim
3900742d1f
Add proton mention (#1306)
* do not add mime-version header if already present

* mention proton in footer

* update email template
2022-09-22 15:15:22 +02:00
Son Nguyen Kim
72a130e225
do not add mime-version header if already present (#1302) 2022-09-22 13:46:32 +02:00
Adrià Casajús
b5aff490ef
Store session in redis if redis is enabled (#1288)
* Store sesions in redis to prevent saving old cookies

* Format

* Rename sid to session_id

* Logout session completely

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-09-21 11:11:17 +02:00
Son Nguyen Kim
2760b149ff
change twitter handle to simplelogin instead of simple_login (#1286) 2022-09-14 17:37:41 +02:00
Adrià Casajús
9c86e1a820
Fix: Use email directly for DomainDeletedAlias (#1273)
* Fix: Use email directly for DomainDeletedAlias

* Add handling for reply phase

* Use the first mailbox of the domain for deleted domain aliase

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-09-08 14:54:32 +02:00
Son Nguyen Kim
753a28e886
handle case msg is string in replace() (#1271)
should fix https://sentry.io/organizations/simplelogin/issues/3563106404/?alert_rule_id=2478639&alert_timestamp=1662404226476&alert_type=email&environment=production&project=1868546&referrer=alert_email
2022-09-07 10:22:11 +02:00
Carlos Quintana
f47661c3d2
Add uncategorized PRs to changelog (#1270) 2022-09-05 16:43:18 +02:00
Son Nguyen Kim
6595d34276
shouldn't count processed batch import (#1268) 2022-09-05 15:38:12 +02:00
Son Nguyen Kim
192d03fd68
make sure sl_formataddr always return str (#1269) 2022-09-05 15:38:04 +02:00
Son Nguyen Kim
313a928070
Create sl_formataddr to handle unicode for built-in formataddr (#1265)
* Create sl_formataddr to handle unicode for built-in formataddr

* fix circular import
2022-09-05 08:40:24 +02:00
PurpleSn0w
48127914c2
Fix: Spelling (#1259)
* Fix: Spelling

* Fix: Spelling

Co-authored-by: Hugh <inbox.xmrjn@simplelogin.co>
2022-09-02 11:58:26 +02:00
Son Nguyen Kim
cea139b7d5
Improve handling when pgp key is invalid (#1264)
* remove unused email statuses

* add more logging

* use text_header if html_header not set

* improve email

* add a header about PGP failure when forward emails can't be encrypted

* remove unused email status
2022-09-02 11:47:04 +02:00
Son Nguyen Kim
25773448c2
admin can go directly to paddle (#1263) 2022-09-02 10:39:53 +02:00
Son Nguyen Kim
96e6753c95
fix dockerfile (#1262) 2022-09-01 16:40:39 +02:00
Son Nguyen Kim
2b389cbe53
use the recommended way to install poetry (#1261) 2022-09-01 15:28:33 +02:00
Son Nguyen Kim
ae2cbf98e2
Handle invalid pgp key (#1260)
* check invalid mailbox pgp key

* check if public key is valid before trying with pgpy

* fix query

* remove unused code
2022-09-01 15:10:11 +02:00
Son Nguyen Kim
f69c9583fb
fix proton partner error when self host (#1255)
* fix proton partner error when self host

* fix test

* fix test

* remove a@b.c
2022-09-01 14:59:16 +02:00
Son Nguyen Kim
72256d935c
do not notify lifetime user about coinbase sub (#1254) 2022-08-30 22:41:08 +02:00
Son Nguyen Kim
fd00100141
fix grammar mistake (#1248) 2022-08-26 16:47:25 +02:00
Son Nguyen Kim
9eacd980ef
include_sender_in_reverse_alias set to true for new users (#1244) 2022-08-23 11:24:49 +02:00
Son Nguyen Kim
b299a305b5
Fix quarantine (#1241)
* add more logging

* fix quarantine email incorrect deleted_at
2022-08-18 14:47:05 +02:00
Carlos Quintana
ba06852dc2
Do not crash if action is unknown (#1231) 2022-08-12 15:02:00 +02:00
Carlos Quintana
7eb44a5947
Fixes for connect with proton on mobile (#1230)
* Fixes for connect with proton on mobile

* Added a test

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-08-12 13:17:21 +02:00
Thanh-Nhon NGUYEN
7476bdde4b
Fix typo in hyperlink to GET /api/user/cookie_token (#1227) 2022-08-12 11:58:31 +02:00
Carlos Quintana
596dd0b1ee
Support next with Proton Link (#1226)
* Support next with Proton Link

* Add support for double next

* Fix bug on account relink
2022-08-11 10:38:44 +02:00
Adrià Casajús
3a75686898
Generate a web session from an api key (#1224)
* Create a token to exchange for a cookie

* Added Route to exchange token for cookie

* add missing migration



Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-08-10 18:48:32 +02:00
Carlos Quintana
a9549c11d7
Rate limiting depending on user authenticated status (#1221)
* Rate limiting depending on user authenticated status

* Update app/extensions.py

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>

* Add rate_limiting tests

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2022-08-09 14:57:21 +02:00
Son Nguyen Kim
a88a8ff2be
add more logging (#1223) 2022-08-09 10:01:55 +02:00
Son Nguyen Kim
6c6deedf47
Stop paddle sub (#1216)
* admin can stop a paddle sub

* show admin menu if user is admin
2022-08-04 09:20:07 +02:00
melbv
f340c9c9ea
DB port correction (#1214)
Correction of the port assigned to PostGresql from '35432' to '5432'
2022-08-03 16:04:03 +02:00
Son Nguyen Kim
69d5de8d41
fix paddle refund (#1213) 2022-08-02 12:43:48 +02:00
Son Nguyen Kim
d72226aa19
show proton sub info on admin (#1207) 2022-08-01 20:49:05 +02:00
Son Nguyen Kim
abe0e0fc46
fix memory error, deleted user when sending newsletter (#1199) 2022-08-01 20:38:13 +02:00
Carlos Quintana
a04152a37f
Do not allow SVG image uploads (#1198) 2022-07-29 08:52:51 +02:00
Adrià Casajús
54466389c5
Update Dockerfile to use python 3.10 (#1195)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-27 18:07:16 +02:00
Adrià Casajús
25fde11a86
Refactor alias suffix (#1194)
* Extract suffix generation and validation to a module

* Updated tests

* Make custom alias use signed suffixes

* Added the signature check to the module

* Fix invalid route

* Move more suffix related stuff

* Fix tests

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-27 17:40:22 +02:00
Adrià Casajús
bd044304f0
Added rate limit to resend activation email (#1192)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-26 14:57:26 +02:00
Adrià Casajús
f4c5198055
Remove ResetCodes after email change (#1191)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-26 14:43:31 +02:00
Son Nguyen Kim
97805173cb
remove envs used for proton beta link (#1189)
* remove envs used for proton beta link

* remove is_connect_with_proton_enabled()
2022-07-26 12:38:18 +02:00
Son Nguyen Kim
c3c0b045db
not blur out other aliases when an alias is highlighted (#1190) 2022-07-26 11:14:33 +02:00
Carlos Quintana
827e3a1acb
Implement mode for Login with Proton (#1186) 2022-07-26 09:55:24 +02:00
Son Nguyen Kim
4f4a098b9b
update wording for proton (#1187)
* update wording for proton

* improve wording
2022-07-25 18:10:30 +02:00
Son Nguyen Kim
125538748d
command send newsletter (#1184) 2022-07-25 11:16:40 +02:00
Son Nguyen Kim
6322e03996
admin can manage newsletter and test sending it (#1177)
* admin can manage newsletter and test sending it

* add comments

* comment

* doc

* not userID not specified, send the newsletter to current user

* automatically match textarea height to content when editing newsletter

* increase text height and limit img size to 100% in email template

* admin can send newsletter to a specific address
2022-07-22 11:24:53 +02:00
Carlos Quintana
7db3ec246e
Mitigate open redirect with OAuth (#1176)
* Mitigate open redirect with OAuth

* Fix tests
2022-07-21 14:23:08 +02:00
Adrià Casajús
598d912f2e
Set ordering to semver (#1175)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-20 12:21:53 +02:00
Adrià Casajús
3fa9db9bb7
Change default unsub behaviour to disable alias by default (#1174)
* Change default unsub behaviour to disable alias by default

* Alter default valut for unsub_behaviouur

* Added comments to the migration

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-20 11:57:34 +02:00
Adrià Casajús
06c1c7f2f7
Restrict the number of free alias for new free users (#1155)
* Restrict the number of free alias for new free users

* Fix test

* Make flag reverse

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-20 11:09:22 +02:00
Son Nguyen Kim
8773ed199a
improve wording (#1168)
* improve wording

* improve wording
2022-07-19 18:58:41 +02:00
Adrià Casajús
f3d47a1eaa
Allow users to keep the original unsub behaviour (#1148)
* Feature: Preserve original unsubscribe request

* Updated tests

* Updated settings

* PR comments

* reduced prefix length

* Include migrate users for new unsub behaviour

* PR comments

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-19 17:25:21 +02:00
Son Nguyen Kim
750b6f9038
distinguish between bounce and quarantine (#1167)
* distinguish between bounce and quarantine

- improve wording
- show bounce or quarantine badge

* prettify
2022-07-19 16:00:02 +02:00
Son Nguyen Kim
c5773af6a8
remove 15 hardcoding (#1164) 2022-07-19 15:09:46 +02:00
Adrià Casajús
afb2ab3758
Allow to configure mem storage from config (#1166)
* Allow to configure memory storage from config

* format

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-19 08:25:59 +02:00
Son Nguyen Kim
36547bd82d
Update wording (#1163)
* rename file

* update wording when adding mailbox

* rename
2022-07-17 15:02:17 +02:00
Adrià Casajús
2837350204
Limit amount of imports (#1161)
* Limit amount of imports

* Review suggestions

* Format

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-16 18:17:15 +02:00
Adrià Casajús
bcd4383e05
Sanitize the highlight contact id (#1160)
* Sanitize also parameter

* Formatting

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-15 17:48:42 +02:00
Adrià Casajús
67be5ba050
Enforce int params in routes (#1159)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-15 17:10:00 +02:00
Son Nguyen Kim
f367acbeaf
Add next bill date on admin UI (#1154)
* add subscription next bill date on admin

* small refactor: remove unused param
2022-07-12 18:17:39 +02:00
Son Nguyen Kim
b742f58829
add a bit of spacing to email template that uses "call" a lot (#1153)
* add a bit of spacing to email template that uses "call" a lot

* apply djlint
2022-07-11 12:06:15 +02:00
Adrià Casajús
2bc088cad7
Disable telegran notificaiton (#1152)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-11 10:27:05 +02:00
Adrià Casajús
f75bdd006a
Fix: Allow internal link independent of enable log in with proton (#1151)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-11 09:41:20 +02:00
Son Nguyen Kim
288f086a55
small rename (#1149) 2022-07-08 09:05:38 +02:00
Adrià Casajús
82d0f44cab
Fix: Check if required session headers exist (#1145)
* Check session keys exist

* Update message

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-05 22:26:48 +02:00
Son Nguyen Kim
6aeb710ca0
add nb_proton_user, nb_proton_premium to daily metric email (#1144) 2022-07-05 18:00:28 +02:00
Adrià Casajús
494005eaa5
Fix: Add weird encodings to the list (#1146)
(cherry picked from commit cfed4061e7bf3e34c52518b905065055acb8858e)

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-05 12:19:14 +02:00
Son Nguyen Kim
8fffe72910
fix refund callback (#1143)
fix https://sentry.io/organizations/simplelogin/issues/3370469626/?alert_rule_id=2478639&alert_timestamp=1656988438946&alert_type=email&environment=production&project=1868546&referrer=alert_email
2022-07-05 10:14:30 +02:00
Carlos Quintana
38d305da23
Bypass 2FA if Login with Proton (#1142)
* Bypass 2FA if Login with Proton

* Fix formatting of template
2022-07-04 16:24:49 +02:00
Adrià Casajús
c2bb6488e4
Allow to login with proton to enter sudo mode (#1141)
* Allow to login with proton to enter sudo mode

* Updated wording

* lint

* Only enabled if the user has the account linked

* Add exit-sudo route for tests

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-04 16:09:36 +02:00
Adrià Casajús
046748c443
Update pre-commit (#1138)
* Update pre-commit

* Upgrade djlint, remove flake8 and add pylint

* Reformat with new djlint version

* Run pre-commit on CI

* Use only python3.10 on CI

* Reformat files with pre-commit

* Run pre-commit against all files

* Reformat

* Added global excludes

* Added pre-commit to the contributing file

* Set python 3.9 as default

* Set language version to python3

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
Co-authored-by: Carlos Quintana <carlos.quintana@proton.ch>
2022-07-04 16:01:04 +02:00
Carlos Quintana
e2f9ea4ae1
Capture exception on Login with Proton (#1140) 2022-07-04 15:40:17 +02:00
Son Nguyen Kim
6d86e64d65
show msg on /internal/integrations/proton (#1139)
* show msg on /internal/integrations/proton

* highlight the connect with Proton section

* djlint
2022-07-04 15:39:12 +02:00
Son Nguyen Kim
2f9301eb97
add 14 days mention and use same stats design for alias activity page (#1136)
* add 14 days mention and use same stats design for alias activity page

* djlint
2022-07-04 11:52:34 +02:00
Adrià Casajús
38c9138cdb
Fix: When logging with parter create accounts with lowercase emails (#1137)
* Fix: When logging with parter create accounts with lowercase emails

* Sanitize emails instead of just lowercase them

* linting

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-04 11:51:43 +02:00
Son Nguyen Kim
66a2152ea3
Compute Proton metrics (#1135)
* compute nb_proton_premium

* compute nb_proton_user
2022-07-04 11:40:29 +02:00
Son Nguyen Kim
02b39f98b7
fix cron job (#1134) 2022-07-04 11:05:42 +02:00
Son Nguyen Kim
8799691f99
allow admin to disable spoofing check on an alias (#1133) 2022-07-04 11:05:13 +02:00
Adrià Casajús
aabcc8e72a
Feature: Add delete account route for the api (#1132)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-02 16:45:58 +02:00
Adrià Casajús
88dd07e48d
Feature: Use new job status to retry killed jobs (#1130)
* Feature: Use new job status to retry killed jobs

* Set attermpts and time via config

* Update timing condition

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-07-01 11:14:53 +02:00
Son Nguyen Kim
93968d00b6
update wording (#1131) 2022-06-30 19:19:22 +02:00
Adrià Casajús
8b89a428e0
Fix: clear next in the session before triggering a login (#1129)
* Fix: clear next in the session before triggering a login

* Format

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-30 15:41:50 +02:00
Adrià Casajús
21feced342
Refactor unsubscribe handling (#1090)
* Refactor unsubscribe email handling

* MR comments

* Moved all unsub logic to the encoder

* remove unused

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-30 11:40:01 +02:00
Adrià Casajús
c85ed7d29e
Fix: Always treat references header as a string (#1127)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-29 19:48:22 +02:00
Son Nguyen Kim
44ddd95730
fix coupon page (#1126) 2022-06-29 18:21:49 +02:00
Carlos Quintana
d06470a3c6
Activate users created with account link (#1124) 2022-06-29 16:55:20 +02:00
Carlos Quintana
9abb8aa47f
Validate user uploaded image (#1123)
* Validate user uploaded image

* Fix test/data path detection
2022-06-29 15:04:55 +02:00
Carlos Quintana
cb7868bdca
Add djlint (#1122)
* Add DJlint configuration

* Initial reformat for djlint

* Add template linting to CI

* Add explanation for HTML template checks in CONTRIBUTING.md
2022-06-29 11:28:26 +02:00
Son Nguyen Kim
f6a7ee981a
do not send double subscription email (#1118)
* do not send double subscription email

* remove unused import

* remove unused test
2022-06-28 17:51:44 +02:00
Son Nguyen Kim
90b767169b
update welcome proton user email (#1119) 2022-06-28 17:29:05 +02:00
Son Nguyen Kim
75c710a6ab
small refactoring (#1120) 2022-06-28 17:21:23 +02:00
Adrià Casajús
aac493ad2f
Update docs and error message for sudo route (#1117)
* Update docs and error message for sudo route

* Fix

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-28 14:40:06 +02:00
Carlos Quintana
07b7f40371
Fix prompt user to upgrade to proton account (#1116) 2022-06-28 12:36:21 +02:00
Son Nguyen Kim
89062edc06
show cancel status in "Current plan" section (#1114)
* show cancel status in "Current plan" section

* do not show upgrade button for canceled paddle sub
2022-06-28 11:58:04 +02:00
Carlos Quintana
dd0598a4dd
Send welcome email when user created by login with proton (#1115)
* Send welcome email when user created by login with proton

* Add dedicated test to user.created_by_partner
2022-06-28 11:57:21 +02:00
Adrià Casajús
5fa41d6ccf
Add state management to job (#1113)
* Add state management to job

* Add migration

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-28 09:22:48 +02:00
Carlos Quintana
686f4f3f68
Always check redirect_uri for oauth (#1111)
* Always check redirect_uri for oauth

* Fix OAuth tests
2022-06-27 13:20:18 +02:00
Carlos Quintana
f58c4a9a50
Show premium subscription managed by partner (#1112) 2022-06-27 13:17:30 +02:00
Adrià Casajús
de31e6d072
Allow to set sudo mode for api requests (#1108)
* Allow to set sudo mode for api requests

* Rebase migration on top of master

* PR comments

* Added missing migration

* Removed unused import

* Apply suggestions from code review

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-23 14:26:36 +02:00
Adrià Casajús
9cc9d38dce
Propose upgrade proton account for proton partner users without paid mail plan (#1106)
* Propose upgrade proton account for proton partner users without paid mail plan

* Reformat js

* Initial display via jinja

* tweak ui: add a ---OR--- separator

* use collapse to show SL upgrade option

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
Co-authored-by: Son <nguyenkims@users.noreply.github.com>
2022-06-23 12:26:02 +02:00
Son Nguyen Kim
09cec0cdec
allow to hide some public domains and set their order (#1107) 2022-06-22 18:21:19 +02:00
Adrià Casajús
db6ec2dbe6
Fix: Missing renamed methods (#1105)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-22 15:34:57 +02:00
Adrià Casajús
99ce10a1bc
Send email to users with a subscription and a partner plan upgrade (#1101)
* Send email to users with a subscription and a partner plan upgrade

* Update double-subscription-partner.html

* Update double-subscription-partner.txt.jinja2

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
Co-authored-by: Son Nguyen Kim <nguyenkims@users.noreply.github.com>
2022-06-20 14:34:20 +02:00
Adrià Casajús
fbb59a1531
Send welcome mail to proton created users (#1099)
* Send welcome mail to proton created users

* Skip import

* Use new logo

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-20 11:36:16 +02:00
Carlos Quintana
fb1e14e509
Add Proton logo to sign up page (#1104) 2022-06-20 09:13:19 +02:00
Carlos Quintana
1798d411a4
Fix hover color in login with proton button (#1100) 2022-06-17 15:35:36 +02:00
Carlos Quintana
5ee5e386e5
Allow to create users from partner (#1095)
* Allow to create users from partner

* Fix tests

* Update tests/test_account_linking.py

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>

* Fix lint

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2022-06-16 10:25:50 +02:00
Carlos Quintana
ba6c5f93ac
Add extension_redirect endpoint (#1093)
* Add extension_redirect endpoint

* Add test for extension_redirect
2022-06-16 09:56:00 +02:00
Carlos Quintana
332fcb27d9
Fix double backslash open redirect (#1096) 2022-06-16 09:55:08 +02:00
Carlos Quintana
58990ec762
Hide proton integration behind cookie (#1092)
* Hide proton integration behind cookie

* Make cookie name configurable via config
2022-06-15 15:42:41 +02:00
Carlos Quintana
b4e3c39329
Add Proton logo to buttons (#1091) 2022-06-15 12:06:11 +02:00
Carlos Quintana
3b47e79fae
Emit events on proton actions (#1089)
* Emit events on proton actions

* Update app/account_linking.py

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>

* Update app/account_linking.py

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2022-06-15 08:24:11 +02:00
Carlos Quintana
cf5ff6fa23
Allow extra headers on proton connection (#1087) 2022-06-14 10:29:18 +02:00
Son Nguyen Kim
39aeb81f9a
add dkim signature for export data email (#1083)
* add dkim signature for export data email

* fix
2022-06-14 10:08:04 +02:00
Son Nguyen Kim
715ce33b09
handle subscription_payment_refunded event (#1075) 2022-06-14 09:41:49 +02:00
Adrià Casajús
3d3d408a8f
Fix: logic failed the onboarding-1 job (#1085)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-14 09:00:52 +02:00
Son Nguyen Kim
83d58c7bca
handle case empty latest_receipt_info (#1081) 2022-06-13 12:42:56 +02:00
Adrià Casajús
efa534fd3e
Store transfer tokens hashed in the db and only allow them to be valid for 24 hours (#1080)
* Store transfer tokens hashed in the db and only allow them to be valid for 30 mins

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-13 12:41:47 +02:00
Son Nguyen Kim
91b3e05ed6
improve wording for data export (#1076) 2022-06-13 08:47:36 +02:00
Carlos Quintana
56ec95bc93
Fix proton integration issues (#1071)
* Fix proton integration issues

* Make external_user_id non nullable

* Fix tests
2022-06-10 16:21:56 +02:00
Son Nguyen Kim
a0a92a7562
require user password before transferring an alias (#1070) 2022-06-10 15:50:44 +02:00
Son Nguyen Kim
0afd414a66
use responseBody.Latest_receipt_info and not responseBody.Receipt.In_app (#1066)
https://developer.apple.com/documentation/appstorereceipts/responsebody/receipt/in_app
2022-06-10 15:50:09 +02:00
Adrià Casajús
a9a44c378a
Do not report complaints for deleted aliases (#1067)
* Do not report complaints for deleted aliases

* revert reorder

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-10 15:44:59 +02:00
Carlos Quintana
c0fe10def6
Raise proper exception on account already linked error (#1069)
* Raise proper exception on account already linked error

* Update app/account_linking.py

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>

* Fix FMT

Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2022-06-10 12:23:04 +02:00
Carlos Quintana
c0a4c44e94
Separate code for proton callback handler (#1040)
* Separate code for proton callback handler

* Upgrade migration

* Use simple_login endpoint from Proton API

* Remove unused classes

* Rename Dto class to Data

* Push rename

* Moved link to PartnerUser to allow subscriptions to depend only on it

* Fix test

* PR comments

* Add unique user_id constraint to PartnerUser

* Added more logs

* Added more logs

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-09 10:19:49 +02:00
Adrià Casajús
faf67ff338
Add missing rate limits (#1065)
Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-06-08 17:36:03 +02:00
Son Nguyen Kim
9cf2f44166
only allow to copy the api key when it is created (#1059)
* show api key created time

* only allow user to copy the api key when it is created

* typo
2022-06-08 10:31:58 +02:00
Son Nguyen Kim
84fcc9ddc4
Notify user cycle email (#1035)
* notify user about a cycle email

* prettify notification detail page
2022-06-07 16:44:57 +02:00
Adrià Casajús
e688f04d6b
Send full user report asynchronously on request (#1029)
* Send full user report asynchronously

* Fix test

* Filter some fields before exporting

* Fix: Domain -> CustomDomain

* format settings html

* not include RefusedEmail as they are not usable by user and are automatically deleted

* send the export to the user email

* change email and setting wording

* fix user can only export data once

* remove alias export section

* remove unused import

* fix flake8

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
Co-authored-by: Son <nguyenkims@users.noreply.github.com>
2022-06-07 10:45:04 +02:00
Thanh-Nhon NGUYEN
cbd44c01f5
Add hyperlinks to endpoints (#1045) 2022-06-04 19:25:21 +02:00
Carlos Quintana
dba56f0dae
Store hmaced partner api tokens (#1028)
* Store hmaced partner api tokens

* MR comments
2022-06-02 11:24:04 +02:00
Adrià Casajús
7ba9bcb9e2
Save unsent emails to disk to be resent later (#1022)
* Initial save to disk

* Store unsent messages to disk so they can be retried later

* Set back not sending emails

* Fixed decorator

* Add general exceptions to the catchall

* Have dummy server just to make sure

* Added several server test cases

* ADded tests for bounced and error status

* Moved dir creation to config parse time

* Set LOG.e

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-05-30 11:52:10 +02:00
Son Nguyen Kim
4a839d9a55
Suggest user to use SL reddit for generic question (#1034) 2022-05-30 09:32:10 +02:00
Adrià Casajús
b30d7d2565
Merge pull request #1001 from simple-login/snyk-upgrade-7758f64956f2190c41945a4353b30f87
[Snyk] Upgrade bootbox from 5.5.2 to 5.5.3
2022-05-26 12:08:04 +02:00
Adrià Casajús
653be79eb2
Merge pull request #1025 from simple-login/dependabot/pip/pyjwt-2.4.0
Bump pyjwt from 2.3.0 to 2.4.0
2022-05-25 14:58:09 +02:00
dependabot[bot]
62882ecaa8
Bump pyjwt from 2.3.0 to 2.4.0
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.3.0...2.4.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-25 06:22:17 +00:00
Son Nguyen Kim
f14e003a38
Merge pull request #1021 from simple-login/ac-verp-unpack
Fix: check if verp return is None before unpack
2022-05-24 08:11:23 +02:00
Adrià Casajús
2b8f7139b8
Fix: check if verp return is None before unpack 2022-05-24 07:54:07 +02:00
Son Nguyen Kim
6b3ff6f9d9
Merge pull request #1014 from simple-login/improve-wording
add mention about the limit of 15 aliases into the header
2022-05-23 17:11:08 +02:00
Adrià Casajús
687b51be0f
Merge pull request #1019 from simple-login/feature/proton-callback-receive-partner_id-as-param
Receive partner as param in ProtonCallbackHandler
2022-05-23 16:49:34 +02:00
Carlos Quintana
5ab943e12c
Remove get_proton_partner_id function 2022-05-23 16:43:06 +02:00
Carlos Quintana
8c6c144ba2
Fix global Partner instance 2022-05-23 16:38:50 +02:00
Carlos Quintana
0064729ca7
Update app/proton/proton_callback_handler.py
Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2022-05-23 16:22:23 +02:00
Carlos Quintana
ed9d2ed816
Receive partner as param in ProtonCallbackHandler 2022-05-23 16:11:58 +02:00
Adrià Casajús
b26d04e82c
Merge pull request #1017 from simple-login/remove-flask-sqlalchemy
remove flask-sqlalchemy and upgrade sqlalchemy from 1.3.19 to 1.3.24
2022-05-23 15:26:03 +02:00
Adrià Casajús
0dfc6c0b0d
Merge pull request #1018 from simple-login/not-redirect-account_activated
redirect user to dashboard instead of the account activation page for now
2022-05-23 15:00:13 +02:00
Son
631254a1cd redirect user to dashboard instead of the account activation page for now 2022-05-23 14:44:24 +02:00
Son
3897d723ea remove flask-sqlalchemy and upgrade sqlalchemy from 1.3.19 to 1.3.24 2022-05-23 14:41:06 +02:00
Son Nguyen Kim
08eb041a28
Merge pull request #1013 from simple-login/menu-optimize
Menu optimize
2022-05-23 08:40:51 +02:00
Adrià Casajús
5c5bafea18
Merge pull request #1016 from simple-login/fix-noreply
Fix sending emails from noreply to an alias not working
2022-05-20 18:16:28 +02:00
Son
e5f23e3517 make sure to only send test emails to user's alias 2022-05-20 18:15:54 +02:00
Son
9e8a419994 add test 2022-05-20 18:12:53 +02:00
Son
0f9232eeeb improve wording 2022-05-20 18:05:05 +02:00
Son
9ba5464bc9 allow to create reverse alias for NOREPLY 2022-05-20 17:59:41 +02:00
Son
53a050d4d1 display user email if user name is empty 2022-05-20 16:35:26 +02:00
Son
b8e3db3e11 add mention about the limit of 15 aliases into the header 2022-05-20 16:28:27 +02:00
Son
471003c631 remove New mention on Subdomain 2022-05-20 16:06:58 +02:00
Son
2a2a72342d do not show SIWSL and Apps page 2022-05-20 16:06:48 +02:00
Son
07f5267c5a move api keys page to header 2022-05-20 16:06:30 +02:00
Son Nguyen Kim
6afe86b395
Merge pull request #1012 from simple-login/account-activated-ui
tweak the account activated page ui
2022-05-20 15:40:47 +02:00
Son Nguyen Kim
d879a0c62b
Merge pull request #1011 from simple-login/ac-fix-recurrent-tests
Fix tests to allow re-running them locally without colliding with previous runs
2022-05-20 15:40:17 +02:00
Son
11e0cbfe9c tweak the account activated page ui 2022-05-20 15:35:57 +02:00
Son Nguyen Kim
cfa46e18fc
Merge pull request #1009 from simple-login/ui-tweak
tweak the UI for onboarding page: use svg instead of png, css change
2022-05-20 15:17:58 +02:00
Son Nguyen Kim
a90e880b24
Merge pull request #1010 from simple-login/fix-upgrade
do not show upgrade button for lifetime user
2022-05-20 15:16:38 +02:00
Son Nguyen Kim
c87e503701
Merge pull request #1004 from simple-login/feature/add-new-page-for-account-activated
Add new page for account activated
2022-05-20 15:16:21 +02:00
Adrià Casajús
ef7dac6da0
Moved pytest.ini to pytest.ci.ini 2022-05-20 14:45:33 +02:00
Adrià Casajús
220f21bb2a
Fix tests to allow re-running them locally without colliding with previous runs 2022-05-20 14:39:07 +02:00
Carlos Quintana
afb7f5ef42
Simplify redirect condition on account_activated 2022-05-20 11:50:50 +02:00
Carlos Quintana
521f6b5822
Update app/onboarding/views/account_activated.py
Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2022-05-20 11:44:49 +02:00
Son
a7d419eec3 do not show upgrade button for lifetime user 2022-05-20 11:00:11 +02:00
Carlos Quintana
f7e27ce0da
Add login_required to the account_activated page 2022-05-20 10:52:46 +02:00
Son
47246d15cf tweak the UI for onboarding page: use svg instead of png, css change 2022-05-20 09:53:11 +02:00
Carlos Quintana
893520c361
Add edge to the browser detection process 2022-05-20 09:46:52 +02:00
Carlos Quintana
a1f37f0841
Detect mobile device and redirect them to dashboard 2022-05-20 09:40:03 +02:00
Carlos Quintana
e5770de329
Add account_activated page prompting user to install the extension 2022-05-20 09:40:03 +02:00
Son Nguyen Kim
0e3be23acc
Merge pull request #997 from simple-login/feature/adapt-extension-setup
Adapt extension setup
2022-05-20 09:01:35 +02:00
Carlos Quintana
7ce4aa6e96
Code quality 2022-05-20 08:58:02 +02:00
Carlos Quintana
6e905f769d
Limit the amount of "PERFORM_EXTENSION_SETUP" messages to be sent 2022-05-20 08:53:45 +02:00
Carlos Quintana
39b5fa50d8
Use is_authenticated 2022-05-20 08:48:01 +02:00
Adrià Casajús
f17043124e
Merge pull request #1000 from simple-login/remove-drag-drop
remove the drag and drop mention for now
2022-05-19 18:39:16 +02:00
Adrià Casajús
76e40894e2
Merge pull request #1003 from simple-login/ac-full-app
Allow testing sent mails and add migrations and templates in app bundle
2022-05-19 12:47:04 +02:00
Adrià Casajús
3330625426
Allow mailer to keep in mem mails to validate tests 2022-05-19 12:27:06 +02:00
Adrià Casajús
74de29cec8
Add migrations and templates in app 2022-05-19 12:24:54 +02:00
Carlos Quintana
e4d6f1f117
Use setInterval instead of setTimeout on the extension 2022-05-19 11:51:18 +02:00
Carlos Quintana
b4da667a5e
Add TODO for removing the cookie on extension onboarding 2022-05-19 11:49:13 +02:00
Carlos Quintana
a73a15d628
Show extension version information on final onboarding screen 2022-05-19 11:47:41 +02:00
Carlos Quintana
e6acff13e5
Send extension setup message if user is logged in 2022-05-19 11:47:22 +02:00
snyk-bot
9595f9d997
fix: upgrade bootbox from 5.5.2 to 5.5.3
Snyk has created this PR to upgrade bootbox from 5.5.2 to 5.5.3.

See this package in npm:
https://www.npmjs.com/package/bootbox

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-05-19 04:28:14 +00:00
Son
8ca4daf894 remove the drag and drop mention for now 2022-05-18 18:29:56 +02:00
Adrià Casajús
e37689dc58
Merge pull request #999 from simple-login/ac-allow-7-bit-encoding
Allow '7-bit' encoding for Content-Transfer-Encoding
2022-05-18 15:01:34 +02:00
Adrià Casajús
cb5fea033f
Merge pull request #998 from simple-login/disabled-mailbox
take into account status.E518
2022-05-18 15:01:11 +02:00
Adrià Casajús
88c60f5387
Allow '7-bit' encoding for Content-Transfer-Encoding 2022-05-18 09:56:30 +02:00
Carlos Quintana
c01db463f7
Hide onboarding contents for a while 2022-05-18 09:22:10 +02:00
Son
11dc28941b take into account status.E518 2022-05-17 18:15:39 +02:00
Carlos Quintana
d3f4602bb7
Send the EXTENSION_SETUP message on /onboarding too 2022-05-17 16:51:08 +02:00
Carlos Quintana
8ac87217d2
Adapt extension setup 2022-05-17 12:22:38 +02:00
Adrià Casajús
a224f4faa6
Merge pull request #995 from simple-login/allow-change-from
Allow to use a different from for send_email()
2022-05-16 19:29:40 +02:00
Son
41c6e8fd79 add quote 2022-05-16 19:23:24 +02:00
Son
e61bf038be allow to use a different from for send_email() 2022-05-16 19:17:56 +02:00
Son Nguyen Kim
4a4d4a5717
Merge pull request #993 from simple-login/update-wording-email
update the email wording
2022-05-16 14:48:35 +02:00
Son
345b3ea4f0 update wording 2022-05-16 14:47:56 +02:00
Adrià Casajús
2adcbf52be
Merge pull request #963 from simple-login/ac-complaints
Handle complaints that have multiple recipients
2022-05-16 10:30:14 +02:00
Adrià Casajús
0da2fd94f1
Set header as a constant 2022-05-16 10:16:42 +02:00
Adrià Casajús
d86a7877a8
Merge pull request #994 from simple-login/remove-admin-notif
no need to notify admin when someone uses a coupon
2022-05-16 10:09:21 +02:00
Son Nguyen Kim
f0263b812e
Merge pull request #986 from simple-login/feature/add-extension-onboarding-pages
Add extension onboarding pages
2022-05-16 09:12:52 +02:00
Carlos Quintana
5fc8245b8b
Remove link to support from test email 2022-05-16 08:27:23 +02:00
Son
54e78786b0 no need to notify admin when someone uses a coupon 2022-05-15 19:57:45 +02:00
Son
f89967f585 update the email wording 2022-05-15 19:51:47 +02:00
Adrià Casajús
3578c61366
Use header 2022-05-13 19:18:20 +02:00
Adrià Casajús
64c67f4429
PR comments 2022-05-13 18:14:21 +02:00
Adrià Casajús
34ad81c7c0
Merge pull request #921 from simple-login/ac-free-no-reverse-alias
Prevent free users from creating reverse-alias
2022-05-13 17:13:48 +02:00
Adrià Casajús
8984d11805
Merge pull request #988 from simple-login/ac-directory-name
Fix: Sanitize directory name before displaying it to the user
2022-05-13 17:10:26 +02:00
Adrià Casajús
3a48b30f30
Fix: Sanitize directory name before displaying it to the user 2022-05-13 16:55:45 +02:00
Carlos Quintana
a0bcb33bd1
Add Or right click to extension onboarding page 2022-05-13 16:13:15 +02:00
Carlos Quintana
2bab0e3e7c
Add Click on the icon to create an alias 2022-05-13 15:05:30 +02:00
Adrià Casajús
3e0cb546a2
Added docs 2022-05-13 14:42:20 +02:00
Adrià Casajús
7235de8e73
HTML formatting 2022-05-13 13:02:26 +02:00
Carlos Quintana
bc48ec0e9f
Add footer for onboarding extension page 2022-05-13 12:17:02 +02:00
Carlos Quintana
2e62a9f00c
Remove support email from test email 2022-05-13 12:16:55 +02:00
Carlos Quintana
bef71b7be3
Update contact instructions on test_email 2022-05-13 10:55:13 +02:00
Carlos Quintana
933237e73b
Implement "Send me an email" button on final extension onboarding 2022-05-13 08:53:31 +02:00
Carlos Quintana
3872626747
Add proton partner on dummy data 2022-05-13 08:29:20 +02:00
Carlos Quintana
710f4d0709
Start adding extension onboarding pages 2022-05-13 08:21:35 +02:00
Adrià Casajús
52cd9d2692
Simplify condition 2022-05-12 19:02:06 +02:00
Adrià Casajús
75dd20ebcc
Fix condition 2022-05-12 19:01:04 +02:00
Adrià Casajús
6e948408c6
Updated api docs 2022-05-12 18:50:27 +02:00
Adrià Casajús
0c896100a4
Update html 2022-05-12 18:46:42 +02:00
Adrià Casajús
bfb1ae6371
PR comments 2022-05-12 18:42:16 +02:00
Adrià Casajús
39b035a123
Added docs 2022-05-12 18:36:12 +02:00
Adrià Casajús
9066116b7e
Simplified method 2022-05-12 18:33:13 +02:00
Adrià Casajús
4d07bc9d31
Moved global flag to config 2022-05-12 18:30:46 +02:00
Adrià Casajús
8b3dc765fa
Revert pytest.ini 2022-05-12 18:21:19 +02:00
Adrià Casajús
514f5c8baa
Merge pull request #981 from simple-login/ac-add-more-info
Action refactor: Only run on push and also send to slack the commit sha
2022-05-12 17:12:04 +02:00
Adrià Casajús
28cc678c5c
Set github sha 2022-05-12 17:06:45 +02:00
Adrià Casajús
bf577a6021
Move scripts to scripts dir 2022-05-12 17:05:28 +02:00
Adrià Casajús
e584268219
Action refactor: Only run on push and also send to slack the sha of the commit 2022-05-12 17:02:03 +02:00
Adrià Casajús
6880fe2150
Merge pull request #980 from simple-login/ci/generate-build-info-on-script
Move generation of build info to script
2022-05-12 16:54:27 +02:00
Carlos Quintana
0ed45f54c6
Move generation of build info to script 2022-05-12 16:48:51 +02:00
Adrià Casajús
caff70ea38
Set global config to enable/disable feature 2022-05-12 16:35:51 +02:00
Carlos Quintana
d6a50ff864
Merge pull request #977 from simple-login/fix/obtain-git-information
Obtain git information from version file
2022-05-12 16:31:13 +02:00
Carlos Quintana
975eacc969
Remove config.SHA1 in favour of build_info.SHA1 2022-05-12 16:26:04 +02:00
Carlos Quintana
9959848d74
Use python version file 2022-05-12 16:21:36 +02:00
Carlos Quintana
c3792dc333
Obtain git information from version file 2022-05-12 16:11:20 +02:00
Carlos Quintana
6b36651def
Fix Slack notification (#976) 2022-05-12 15:22:40 +02:00
Adrià Casajús
19e30eaf0a
New migration 2022-05-12 13:42:53 +02:00
Adrià Casajús
5dde39eb37
Prevent free users from creating reverse-alias 2022-05-12 13:20:05 +02:00
Adrià Casajús
2660c96fa7
Fix: Track processes that start with the same chars independently (smtp vs stmpd) (#974)
* Fix: Track processes that start with the same chars independently (smtp vs stmpd)

* Format

* Added more test

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
2022-05-12 12:37:19 +02:00
Carlos Quintana
79f6b2235e
Merge pull request #972 from simple-login/ci/auto-generate-release-notes
Generate release notes when creating a tag
2022-05-12 12:24:10 +02:00
Carlos Quintana
7b71eab5d4
Update .github/changelog_configuration.json
Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2022-05-12 12:17:27 +02:00
Carlos Quintana
69e2f48d13
Try to generate release notes 2022-05-12 12:11:45 +02:00
Adrià Casajús
dde25678cd
Merge pull request #953 from simple-login/fix-default-domain
handle case user doesn't have default domain for alias
2022-05-12 11:18:53 +02:00
Adrià Casajús
b17af67614
Merge pull request #969 from simple-login/ac-push-on-tag
GH Actions refactor
2022-05-12 09:57:09 +02:00
Carlos Quintana
e01bae6206
Fix create sentry release 2022-05-12 09:12:10 +02:00
Carlos Quintana
cbcd4ea92f
Add slack message sending + upgrade docker build process 2022-05-12 08:54:44 +02:00
Adrià Casajús
9f43a33c09
Merge pull request #970 from simple-login/paddle-grace-period
use a grace period of 14 days for paddle subscription
2022-05-11 19:46:38 +02:00
Adrià Casajús
bc41fdec35
Merge pull request #968 from simple-login/ac-monitor-procs
Add monitoring for postfix processes
2022-05-11 19:46:00 +02:00
Son
f3b41279a9 simplify template 2022-05-11 19:12:52 +02:00
Son
7d591baea5 handle case user doesn't have default domain for alias
when user doesn't have default domain for alias, display "Not Selected" to avoid confusion
2022-05-11 19:10:02 +02:00
Son
d2fad44003 create a constant for paddle grace days 2022-05-11 19:03:27 +02:00
Son
2573c68e82 use a grace period of 14 days for paddle subscription 2022-05-11 17:24:52 +02:00
Adrià Casajús
b3645b33dd
Set global change for the job 2022-05-11 16:48:27 +02:00
Adrià Casajús
4f8a590ef9
Run only on pull 2022-05-11 16:35:08 +02:00
Adrià Casajús
42159dce4d
Run docker build only once and also on tag 2022-05-11 16:33:24 +02:00
Adrià Casajús
924f286db1
Add monitoring for postfix processes 2022-05-11 15:30:09 +02:00
Adrià Casajús
a8c01a1443
Merge pull request #967 from simple-login/fix/open-redirect
Fix open redirect
2022-05-11 14:57:39 +02:00
Carlos Quintana
e09d7a2b71
Fix open redirect 2022-05-11 14:50:37 +02:00
Adrià Casajús
dc51ad4f11
Merge pull request #964 from simple-login/paddle-fix
Improve paddle handling
2022-05-11 14:24:49 +02:00
Carlos Quintana
243fc7b7ab
Merge pull request #966 from simple-login/chore/upgrade-newrelic-sdk
Upgrade newrelic sdk
2022-05-11 12:13:10 +02:00
Carlos Quintana
709d5a8866
Upgrade newrelic sdk 2022-05-11 12:01:12 +02:00
Adrià Casajús
3aab1e02b5
Merge pull request #962 from simple-login/chore/upgrade-sentry-sdk
Upgrade sentry sdk
2022-05-11 10:39:18 +02:00
Adrià Casajús
48554369bd
Get the mailbox if possible from the email log 2022-05-10 23:34:57 +02:00
Son
e1ca90a28e log when subscription not exist 2022-05-10 18:53:21 +02:00
Son
998e1d7aef handle subscription cancel from deleted user 2022-05-10 18:51:04 +02:00
Adrià Casajús
d2111d4768
Added doc comments 2022-05-10 18:26:56 +02:00
Adrià Casajús
6c13f7de05
refactored to reduce duplicated codepaths 2022-05-10 18:23:14 +02:00
Adrià Casajús
a2f141d3cc
Get recipient address from the complaint report when possible 2022-05-10 17:54:51 +02:00
Carlos Quintana
26da6ea7c5
Upgrade sentry sdk 2022-05-10 16:09:21 +02:00
Son Nguyen Kim
b70170cf0c
Merge pull request #960 from simple-login/fix-verp
Fix verp: take into account phase
2022-05-10 15:04:14 +02:00
Son
21255866b6 improve test 2022-05-10 14:53:50 +02:00
Son
b1b3c15a9f fix 2022-05-10 11:23:52 +02:00
Son
4dbbc4ed5e add test 2022-05-10 11:23:40 +02:00
Son
44b0aba4f3 fix verp generation 2022-05-10 11:19:25 +02:00
Adrià Casajús
8b80b72665
Merge pull request #959 from simple-login/feature/oauth-login-preserve-next-url
Preserve next_url in oauth login
2022-05-10 11:18:07 +02:00
Son Nguyen Kim
22e554b785
Merge pull request #955 from simple-login/update-crypto
upgrade cryptography to 37.0.1
2022-05-10 10:02:53 +02:00
Carlos Quintana
5b60ef1e35
Preserve next_url in oauth login 2022-05-09 12:20:14 +02:00
Son Nguyen Kim
7e00dfddc3
Merge pull request #954 from simple-login/fix-proton-login
allow the code to run without proton partner
2022-05-09 08:25:07 +02:00
Adrià Casajús
3ccca2e02e
Merge pull request #957 from simple-login/feature/add-oauth-hook-check-status-code
Add OAuth hook for checking the status code
2022-05-06 16:16:40 +02:00
Carlos Quintana
c95bfb80a2
Add OAuth hook for checking the status code 2022-05-06 14:41:52 +02:00
Son
4e2baf0169 upgrade cryptography to 37.0.1
The current version "3.3.2" can't seem to be installed on mac
2022-05-05 15:07:17 +02:00
Son
b720dfc381 allow the code to run without proton partner 2022-05-05 15:05:39 +02:00
Adrià Casajús
a92981c52d
Merge pull request #894 from cquintana92/feature/add-login-with-proton
Add login with proton
2022-05-05 12:29:00 +02:00
Carlos Quintana
8d4683e59e
Add login with proton 2022-05-05 12:20:55 +02:00
Adrià Casajús
f2d761c61b
Merge pull request #950 from simple-login/ac-deprecate-old-verp
Remove deprecated verp email validation
2022-05-05 10:07:57 +02:00
Son Nguyen Kim
6d49ae62d4
Merge pull request #952 from alpha-tango-kilo/ssl
Update SSL documentation
2022-05-04 18:40:36 +02:00
alpha-tango-kilo
cb177a10a2
Update SSL documentation
Improve readability
Talk about HSTS
Link to SSL doc in README
2022-05-04 17:09:35 +01:00
Adrià Casajús
c48247e852
Remove deprecated verp email validation 2022-05-04 16:17:51 +02:00
Adrià Casajús
67dad33b70
Merge pull request #947 from simple-login/ac-sync-model
Align db with models
2022-05-03 17:29:49 +02:00
Adrià Casajús
66c6db773f
Align db with models for the audit_log 2022-05-03 16:48:54 +02:00
Adrià Casajús
eea436875a
Merge pull request #914 from simple-login/ac-store-contact-bounces
Store bounces in the reply phase to prevent abuse
2022-05-03 14:44:07 +02:00
Adrià Casajús
6936d99779
Set default state for provider complaint 2022-05-03 14:16:04 +02:00
Son Nguyen Kim
beea14ef14
Update provider-complaint-reply-phase.txt.jinja2 2022-05-02 16:41:37 +02:00
Adrià Casajús
56159765d9
Rename 2022-05-02 11:53:32 +02:00
Son Nguyen Kim
65bc6c7fdf
Merge pull request #945 from simple-login/run-migration-in-ci
Run migration in ci
2022-05-02 10:20:12 +02:00
Son
2de5161cd2 run alembic in run-test.sh 2022-05-01 17:38:14 +02:00
Son
e74362dd9f set config 2022-05-01 17:22:05 +02:00
Son
c748ab22e6 run db migration in github ci 2022-05-01 17:06:10 +02:00
Adrià Casajús
ba46ce5208
Format 2022-04-29 16:02:45 +02:00
Adrià Casajús
baddc0fe67
Fix: sqlalchemy only suports str as server_default 2022-04-29 15:58:48 +02:00
Adrià Casajús
e62022f032
Merge remote-tracking branch 'origin/master' into ac-store-contact-bounces
* origin/master: (29 commits)
  PR comments
  support "enabled" param in /api/v2/aliases
  Update PGPy to 0.5.4 to allow for python 3.10
  Also install libpq-dev
  Fix python 3.10
  Add methods to check if alias will be auto-created
  PR comments
  Allow sending messages in a background thread
  Use the proper import for newrelic agent
  not send emails to inform about an alias can't be created to disabled user
  prevent disabled user from using the api
  make sure disabled user can't create new alias
  Put version version between " so it is 3.10 instead of 3.1
  Add workflow for python 3.10
  Remove it for all creds
  Do not send the transports to the js part since we have not stored them previously
  move help to menu on small screen
  only show the help button on desktop
  use another logo for mobile
  add new parameter disabled in /GET /api/v2/aliases
  ...
2022-04-29 15:56:09 +02:00
Adrià Casajús
cca709ed48
formatting 2022-04-29 15:50:52 +02:00
Adrià Casajús
884407d6c8
Merge pull request #934 from simple-login/ac-test-with-python310
Add test workflow for python 3.10
2022-04-29 15:27:28 +02:00
Adrià Casajús
04399e827e
Merge pull request #940 from simple-login/ac-check-auto-create
Add methods to check if an alias will be auto-created
2022-04-29 12:05:18 +02:00
Son Nguyen Kim
3fa820fc2b
Merge pull request #941 from simple-login/enable-filter
support "enabled" param in /api/v2/aliases
2022-04-29 09:35:52 +02:00
Adrià Casajús
1f040fcebc
PR comments 2022-04-28 18:43:10 +02:00
Son
a2c477a816 support "enabled" param in /api/v2/aliases 2022-04-28 17:24:35 +02:00
Adrià Casajús
46646f4ee2
Merge pull request #932 from simple-login/ac-fix-webauthn-transport
Do not send the transports to the js part since we have not stored them previously
2022-04-28 16:53:00 +02:00
Adrià Casajús
bb4207c3a1
Merge pull request #938 from simple-login/ac-fix-invalid-import
Use the proper import for newrelic agent
2022-04-28 16:52:09 +02:00
Adrià Casajús
7190df9c4e
Merge pull request #939 from simple-login/ac-bg-sending
Allow sending messages in a background thread
2022-04-28 16:51:54 +02:00
Adrià Casajús
89fe4387e5
Update PGPy to 0.5.4 to allow for python 3.10 2022-04-28 16:20:14 +02:00
Carlos Quintana
8fedceb090
Also install libpq-dev 2022-04-28 16:10:43 +02:00
Adrià Casajús
74b31eac66
PR comments 2022-04-28 15:24:45 +02:00
Carlos Quintana
0a34c1547f
Fix python 3.10 2022-04-28 15:24:42 +02:00
Adrià Casajús
7fd9bdc5a7
PR comments 2022-04-28 15:23:52 +02:00
Adrià Casajús
8e35a09788
Add methods to check if alias will be auto-created 2022-04-28 15:10:38 +02:00
Adrià Casajús
f9a390c1a2
PR comments 2022-04-28 15:03:14 +02:00
Adrià Casajús
9a04376894
Allow sending messages in a background thread 2022-04-28 14:43:24 +02:00
Adrià Casajús
25c3626226
Use the proper import for newrelic agent 2022-04-28 13:02:45 +02:00
Son Nguyen Kim
93ae82aa46
Merge pull request #936 from simple-login/disable-user-cannot-use-api
prevent disabled user from using the api
2022-04-28 12:13:14 +02:00
Son Nguyen Kim
b85f0952a5
Merge pull request #937 from simple-login/not-inform-disabled-account
not send emails to inform about an alias can't be created to disabled account
2022-04-28 12:12:54 +02:00
Son
845b53b03f not send emails to inform about an alias can't be created to disabled user 2022-04-28 12:10:40 +02:00
Son
7b7cb0b571 prevent disabled user from using the api 2022-04-27 16:24:38 +02:00
Son Nguyen Kim
69d1875be1
Merge pull request #935 from simple-login/disable-user-cannot-create-new-alias
make sure disabled user can't create new alias
2022-04-27 16:15:57 +02:00
Son
eab7606f93 make sure disabled user can't create new alias 2022-04-27 16:06:54 +02:00
Adrià Casajús
7d38c41d52
Put version version between " so it is 3.10 instead of 3.1 2022-04-27 15:30:16 +02:00
Adrià Casajús
83a8d439e5
Add workflow for python 3.10 2022-04-27 15:28:26 +02:00
Son Nguyen Kim
2fc2c85c5e
Merge pull request #931 from simple-login/fix-mobile-view
Fix mobile view
2022-04-27 12:46:17 +02:00
Adrià Casajús
657cae53a6
Remove it for all creds 2022-04-26 18:44:57 +02:00
Adrià Casajús
ff33380bed
Do not send the transports to the js part since we have not stored them previously 2022-04-26 18:41:12 +02:00
Son
d1447e293d move help to menu on small screen 2022-04-26 15:47:25 +02:00
Son
e01eff8755 only show the help button on desktop 2022-04-26 13:01:12 +02:00
Son
f6320d5321 use another logo for mobile 2022-04-26 13:00:57 +02:00
Son Nguyen Kim
f5a5a06e19
Merge pull request #927 from simple-login/return-disable-alias
add new parameter disabled in /GET /api/v2/aliases
2022-04-25 18:49:58 +02:00
Adrià Casajús
5208c549fa
Rename TransactionalComplaint to ProviderComplaint 2022-04-25 14:40:42 +02:00
Son
58b332b7bc add new parameter disabled in /GET /api/v2/aliases 2022-04-25 09:22:29 +02:00
Adrià Casajús
fcd2ab6fed
Set data to non-nullable 2022-04-22 14:53:04 +02:00
Adrià Casajús
89d94963d7
PR comments 2022-04-22 14:49:03 +02:00
Son Nguyen Kim
5053d9f1f5
Merge pull request #918 from simple-login/handle-error-as-bytes
handle the AttributeError that can also be raised by as_bytes()
2022-04-22 10:51:55 +02:00
Son
8cf58d7e24 add B001 to flake8 2022-04-22 10:39:01 +02:00
Son
af1c2e5556 allow bare except in flake8 2022-04-22 10:36:19 +02:00
Son
68ec159d91 catch all exception in to_bytes 2022-04-22 10:20:43 +02:00
Son Nguyen Kim
2bcc22c391
Merge pull request #906 from simple-login/ac-hash-change
Support python>3.8 for verp emails and reduce size by truncating hmac and storing time in minutes since 2022-01-01
2022-04-22 10:11:31 +02:00
Son Nguyen Kim
e5943dcdc6
Merge pull request #919 from simple-login/codeowners
create  CODEOWNERS file for auto PR reviewers
2022-04-22 10:07:34 +02:00
Son Nguyen Kim
a886fb70f2
Merge pull request #923 from cquintana92/feature/allow-simplelogin-to-be-used-as-dependency
Allow SimpleLogin to be used as a dependency
2022-04-22 08:34:06 +02:00
Carlos Quintana
d0dcf1f148
Allow SimpleLogin to be used as a dependency 2022-04-22 08:26:37 +02:00
Adrià Casajús
0f14c3e74e
Move some comments as docstrings 2022-04-21 15:25:06 +02:00
Son
db8359fca6 create CODEOWNERS file for auto PR reviewers 2022-04-21 11:33:30 +02:00
Adrià Casajús
112b2c77c3
Add backwards compat with shake128 signed verp emails 2022-04-21 11:30:39 +02:00
Son
0f7ccec51a handle the AttributeError that can also be raised by as_bytes() 2022-04-21 11:28:11 +02:00
Adrià Casajús
c573ef655e
Store bounces in the reply phase to prevent abuse 2022-04-21 11:23:58 +02:00
Son Nguyen Kim
99d31698e7
Merge pull request #917 from simple-login/noreplies
able to handle several noreply addresses
2022-04-21 10:58:21 +02:00
Son
b61670fbc0 remove unused import 2022-04-21 09:26:44 +02:00
Son
b3bb0cf250 black 2022-04-21 09:17:09 +02:00
Son
bddb5e500a able to handle several noreply addresses
This prepares the change of noreply@simplelogin.co to noreply@simplelogin.io
2022-04-21 08:59:46 +02:00
Adrià Casajús
af24876c71
Use sha3 and truncate to 8 bytes and store time in minutes starting at 2022-01-01 2022-04-20 20:46:35 +02:00
Son Nguyen Kim
0dae9091ab
Merge pull request #907 from simple-login/contribution-change
move the contact us section on top of the contributing
2022-04-20 09:11:17 +02:00
Son Nguyen Kim
a26927f96e
Update CONTRIBUTING.md
Co-authored-by: Adrià Casajús <acasajus@users.noreply.github.com>
2022-04-20 09:10:40 +02:00
Son Nguyen Kim
c14e01839e
Merge pull request #899 from simple-login/add-alias-to-to-header
add alias to To: header if it isn't included in To and Cc header
2022-04-20 09:10:11 +02:00
Son
b545ebaeb1 remove unnecessary test 2022-04-19 19:40:50 +02:00
Son
01fd880902 add more test. Make sure to delete To header before changing. 2022-04-19 18:45:59 +02:00
Son
0f1e290461 move the contact us section on top of the contributing 2022-04-19 18:38:15 +02:00
Son Nguyen Kim
9b624edf11
Merge pull request #901 from simple-login/no-dot-in-reverse-alias
use _ instead of . in reverse alias
2022-04-19 18:24:37 +02:00
Son
e136fc8c92 add test 2022-04-19 13:33:31 +02:00
Son Nguyen Kim
6eb6283c78
Merge pull request #905 from simple-login/ac-fix-incorrect-padding
Calculate proper padding when decoding base32
2022-04-19 10:59:09 +02:00
Adrià Casajús
bad9202cf8
Calculate proper padding when decoding base32 2022-04-19 10:50:25 +02:00
Son Nguyen Kim
259851a04e
Merge pull request #860 from acasajus/remove-softfail
Generate secure transactional emails from address
2022-04-19 09:28:47 +02:00
Son
becde6458b fix test 2022-04-18 11:55:14 +02:00
Son
9a994ec98b fix test 2022-04-18 11:17:10 +02:00
Son
aaccfc6f9d fix test 2022-04-18 10:18:51 +02:00
Son
199ec09554 update bounce warning email to avoid having sender address in the email subject 2022-04-18 09:30:29 +02:00
Son
cb8b20fc9a fix test 2022-04-15 17:37:19 +02:00
Son
8dfdac79bf use _ instead of . in reverse alias
to avoid AC_FROM_MANY_DOTS SpamAssassin rule
2022-04-15 17:34:29 +02:00
Son Nguyen Kim
6f7ab01487
Merge pull request #900 from simple-login/refactor-test
refactor test: no hardcode a@b.c, make sure each test has a different user
2022-04-15 17:07:53 +02:00
Son
06874ea97c fix test 2022-04-15 17:06:00 +02:00
Son
0565ca4d5e add alias to To: header if it isn't included in To and Cc header 2022-04-15 17:01:27 +02:00
Son
a966665478 refactor test: no hardcode a@b.c, make sure each test has a different user 2022-04-15 16:59:44 +02:00
Son Nguyen Kim
72464bd959
Merge pull request #897 from simple-login/alias-option-endpoint
return whether a domain is custom or primary in GET /api/v5/alias/options
2022-04-14 19:10:07 +02:00
Son
7edbc3a5d5 black 2022-04-14 18:53:27 +02:00
Son
217518c00e refactor 2022-04-14 18:37:55 +02:00
Adrià Casajús
d28980a810
Format 2022-04-14 18:27:20 +02:00
Adrià Casajús
4bcc728222
Merge remote-tracking branch 'origin/master' into remove-softfail
* origin/master: (34 commits)
  fix flake8
  add link to the anti phishing page
  improve email wording
  Move tests
  Only send enum names
  Only send enum name for events intead of the full class.enum
  Also track login and register events from the api routes
  typo
  revert changes
  Added fix for parts that are not messages
  Add missing formatting place
  Revert unwanted changes
  Do not show an error if we receive an unsubscribe from a different address
  Revert changes to pgp_utils
  fix import
  Send newrelic events on login and register
  PR changes
  format
  Move dmarc management to its own file
  ignore VERPTransactional
  ...
2022-04-14 18:25:03 +02:00
Son
debed67c68 return whether a domain is custom or primary in GET /api/v5/alias/options 2022-04-14 17:28:40 +02:00
Son
a957cbb3c0 fix flake8 2022-04-14 09:47:58 +02:00
Son
1709de93ef add link to the anti phishing page 2022-04-14 09:28:26 +02:00
Son
95770de4d5 improve email wording 2022-04-14 09:23:49 +02:00
Son Nguyen Kim
80a45b4b07
Merge pull request #872 from simple-login/ac-dmarc-reply-phase
Apply dmarc policy to the reply phase
2022-04-12 18:25:32 +02:00
Adrià Casajús
fc13171f3d
Move tests 2022-04-12 12:51:11 +02:00
Adrià Casajús
ca93c8e603
Merge remote-tracking branch 'origin/master' into ac-dmarc-reply-phase
* origin/master:
  Only send enum name for events intead of the full class.enum
  Also track login and register events from the api routes
  typo
  revert changes
  Added fix for parts that are not messages
  Add missing formatting place
  Revert unwanted changes
  Do not show an error if we receive an unsubscribe from a different address
  Revert changes to pgp_utils
  Send newrelic events on login and register
2022-04-12 12:48:46 +02:00
Son Nguyen Kim
2fd3d268e9
Merge pull request #891 from simple-login/ac-fix-event-attributes
Only send enum name for events intead of the full class.enum
2022-04-12 11:12:35 +02:00
Adrià Casajús
0f91effce9
Only send enum names 2022-04-12 09:34:05 +02:00
Adrià Casajús
9928525cf9
Only send enum name for events intead of the full class.enum 2022-04-12 09:04:57 +02:00
Son Nguyen Kim
7a0fd34823
Merge pull request #886 from simple-login/ac-fix-unauthorized-email
Do not assume all parts in multipart messages are processed as messages
2022-04-11 17:54:35 +02:00
Son Nguyen Kim
0a9c103ad1
Merge pull request #884 from simple-login/ac-login-metric
Send newrelic events on login and register
2022-04-11 17:52:52 +02:00
Adrià Casajús
2b149747f5
Also track login and register events from the api routes 2022-04-11 16:11:01 +02:00
Adrià Casajús
8da4293305
typo 2022-04-11 16:04:28 +02:00
Adrià Casajús
edf34656b6
revert changes 2022-04-11 15:53:37 +02:00
Adrià Casajús
c16fd25b2e
Added fix for parts that are not messages 2022-04-11 15:52:31 +02:00
Adrià Casajús
dbc55c50a2
Add missing formatting place 2022-04-11 14:51:33 +02:00
Son Nguyen Kim
9d6ba0a9b3
Merge pull request #885 from simple-login/ac-fix-unauthorized-email
Do not show an error if we receive an unsubscribe from a different address
2022-04-11 14:45:40 +02:00
Adrià Casajús
ae8824a356
Revert unwanted changes 2022-04-11 14:20:56 +02:00
Adrià Casajús
7649f6b822
Do not show an error if we receive an unsubscribe from a different address 2022-04-11 14:19:32 +02:00
Adrià Casajús
dc59b61fba
Revert changes to pgp_utils 2022-04-11 10:20:02 +02:00
Adrià Casajús
f333bb00c5
fix import 2022-04-11 10:19:25 +02:00
Adrià Casajús
60a070731e
Send newrelic events on login and register 2022-04-11 10:18:22 +02:00
Adrià Casajús
7fdd7d7f6a
PR changes 2022-04-11 09:28:57 +02:00
Adrià Casajús
0dbe504329
format 2022-04-08 14:23:59 +02:00
Adrià Casajús
8df6d98522
Merge remote-tracking branch 'origin/master' into ac-dmarc-reply-phase 2022-04-08 11:34:12 +02:00
Adrià Casajús
68e58c0876
Move dmarc management to its own file 2022-04-08 11:28:14 +02:00
Son
42f89b71d7 ignore VERPTransactional 2022-04-08 11:10:54 +02:00
Son
d26fc6ecf0 update email wording 2022-04-08 11:10:43 +02:00
Adrià Casajús
b128d64563
Moved spamd check to a custom file and cached the result 2022-04-07 19:17:37 +02:00
Son Nguyen Kim
a611b90593
Merge pull request #873 from simple-login/ac-save-full-envelope-for-debug
Save original envelope for debugging
2022-04-06 17:55:31 +02:00
Adrià Casajús
44c77439c1
PR comments 2022-04-06 17:44:05 +02:00
Adrià Casajús
33e83fc153
fix message 2022-04-06 17:37:55 +02:00
Adrià Casajús
0e3c46d944
Save original envelope for debugging 2022-04-06 17:31:46 +02:00
Adrià Casajús
61b8bbdfcc
Fix tests 2022-04-06 17:07:36 +02:00
Adrià Casajús
8ca1be0166
Apply dmarc policy to the reply phase 2022-04-06 12:51:04 +02:00
Son Nguyen Kim
936fa17005
Merge pull request #870 from simple-login/fix/misc
Set CONTENT_TRANSFER_ENCODING if absent
2022-04-05 19:13:15 +07:00
Son
754bd4964c Set CONTENT_TRANSFER_ENCODING if absent 2022-04-05 11:56:45 +02:00
Son
9aeceb9119 change logging for icloud bounce case 2022-04-05 11:52:43 +02:00
Son
43a6c87fd6 format some html files using pycharm 2022-04-02 17:36:33 +07:00
Son
c83bea6650 improve wording 2022-04-02 17:11:42 +07:00
Adrià Casajús
26889283d3
format 2022-03-30 17:20:49 +02:00
Adrià Casajús
c9a15f4921
Fixed tests 2022-03-30 16:29:38 +02:00
Adrià Casajús
451e69a3c4
More rebase fixes 2022-03-30 16:09:17 +02:00
Son Nguyen Kim
358d777b9e
Merge pull request #865 from simple-login/dmarc-soft-fail
Add a warning message to the email when it dmarc softfail
2022-03-30 21:08:36 +07:00
Adrià Casajús
dce9e633bf
fix 2022-03-30 16:02:05 +02:00
Adrià Casajús
db06ce0ae6
Create signed email addresses for VERP emails 2022-03-30 16:00:02 +02:00
Son
215561dec1 fix test 2022-03-30 20:54:42 +07:00
Son
1b5521efcf use red color for warning 2022-03-30 19:48:07 +07:00
Son
67c2c6afad add warning to email content when dmarc softfail 2022-03-30 19:48:07 +07:00
Son Nguyen Kim
110f2f2f2c
Merge pull request #861 from acasajus/spf-dmarc-backscatter
Reduce backscatter by checking return-path domain SPF status
2022-03-30 19:44:39 +07:00
Son Nguyen Kim
f7a98bc7d2
Merge pull request #862 from simple-login/ac/sanitize-next
Properly validate //host.com urls
2022-03-30 19:40:36 +07:00
Adrià Casajús
87ec7e05de
Revert pytest.ini 2022-03-30 11:56:15 +02:00
Adrià Casajús
83fc8964a8
PR comments 2022-03-30 09:53:35 +02:00
Son Nguyen Kim
d561bae7dd
Merge pull request #864 from simple-login/ac/insecure-random
Replace using random with secrets for security purposes
2022-03-30 11:49:33 +07:00
Son Nguyen Kim
90508c7ee7
Merge pull request #863 from simple-login/ac/sanitize-rate-limit
Add limiters to auth routes
2022-03-30 11:44:44 +07:00
Adrià Casajús
1555bc6346
fix test 2022-03-29 21:03:55 +02:00
Adrià Casajús
19e87a7156
More random to secrets 2022-03-29 18:42:28 +02:00
Adrià Casajús
b15facb6e4
Use secrets instead of random 2022-03-29 18:40:52 +02:00
Adrià Casajús
77faff5f7c
reverted pytest 2022-03-29 18:37:22 +02:00
Adrià Casajús
97ef5ff765
Fix oauth redirect when clientid is invalid 2022-03-29 18:37:01 +02:00
Adrià Casajús
a9e31cff26
Fix tests 2022-03-29 18:34:13 +02:00
Adrià Casajús
c5b0f5304e
Format 2022-03-29 18:18:11 +02:00
Adrià Casajús
d6df5e0ea0
Add limiters to auth routes 2022-03-29 18:14:13 +02:00
Adrià Casajús
e91fd26964
Sanitized missing places 2022-03-29 18:03:18 +02:00
Adrià Casajús
8963a92f30
Revert pytest 2022-03-29 17:53:58 +02:00
Adrià Casajús
fe9161b101
Properly validate //host.com urls when redirecting after receiving a next param 2022-03-29 17:53:00 +02:00
Adrià Casajús
ac9b88f87d
Add no header test 2022-03-29 15:59:35 +02:00
Adrià Casajús
085c166cb2
Replace 5XX with 2XX for return path that fail SPF check 2022-03-29 15:09:10 +02:00
Adrià Casajús
7d36256b7c
Check return-path spf record before bouncing a message 2022-03-29 10:52:11 +02:00
Son
b0023981af change log 2022-03-25 18:28:31 +01:00
Son Nguyen Kim
af85b3a997
Merge pull request #856 from simple-login/feature/dmarc-email-notif
Send email when an email is put to quarantine and do not put soft_fail email to quarantine
2022-03-25 18:26:30 +01:00
Son
8820cecdd3 comment out soft_fail test 2022-03-25 18:12:33 +01:00
Son
4dbe22d856 do not put soft_fail email into quarantine for now 2022-03-25 18:04:53 +01:00
Son
0d7d56c0ea send email when an email is put to quarantine 2022-03-25 18:02:17 +01:00
Son Nguyen Kim
beee438445
Merge pull request #855 from simple-login/fix/dmarc
Fix/dmarc
2022-03-25 18:01:28 +01:00
Son
159d30820e change N/A to unknown to avoid confusion 2022-03-25 18:00:49 +01:00
Son
75da6d7027 fix 2022-03-25 16:49:49 +01:00
Son
334365e853 Merge branch 'master' into fix/dmarc
# Conflicts:
#	email_handler.py
2022-03-25 16:24:12 +01:00
Son
17d9190309 log envelope mail_from and header_from when dmarc fail 2022-03-25 16:20:30 +01:00
Son
63b1100a8b log event when there's no dmarc result 2022-03-25 16:19:11 +01:00
Son
ce2d2a3b3a fix case where header isn't string 2022-03-25 16:17:58 +01:00
Son Nguyen Kim
2a4d2d723b
Merge pull request #853 from acasajus/newrelic-event
Rename newrelic dmarc event
2022-03-24 12:53:02 +01:00
Adrià Casajús
e5fa90cf04
Rename newrelic dmarc event 2022-03-24 12:51:58 +01:00
Son
32fd65b69b add more log for alias transfer 2022-03-23 18:33:33 +01:00
Son
37de10e54c fix dmarc_result can be None 2022-03-23 08:34:25 +01:00
Son
cb92f1efea log when an email fails dmarc 2022-03-22 18:54:45 +01:00
Son
e11c257571 improve notification for quarantine 2022-03-22 18:54:36 +01:00
Son
4fc450720f fix test 2022-03-22 17:44:08 +01:00
Son Nguyen Kim
3d30870395
Merge pull request #849 from acasajus/new/parse-rpamd-headers
Return 200 on fishy dmarc result
2022-03-22 17:36:45 +01:00
Son Nguyen Kim
99b05034b0
Merge pull request #843 from acasajus/new/parse-rpamd-headers
Parse rspamd headers and apply dmarc policy if found.
2022-03-22 17:13:11 +01:00
Adrià Casajús
517bcb632e
MR changes 2022-03-22 17:02:59 +01:00
Son Nguyen Kim
ed92941bed
Merge pull request #848 from acasajus/fix-reply-to-mail
Fix transactional emails support
2022-03-22 14:26:40 +01:00
Adrià Casajús
51b479c64f
Fix transactional emails 2022-03-22 12:23:16 +01:00
Son
5b3688b6df set a domain for message-id 2022-03-22 11:02:02 +01:00
Adrià Casajús
ce6ee1a105
Added checks to get_dmarc_status 2022-03-21 19:13:51 +01:00
Adrià Casajús
93b06fe30c
Keep original From 2022-03-21 19:05:15 +01:00
Adrià Casajús
1b2d504b3b
Send a notification to the user when a message has been quarantined 2022-03-21 18:33:18 +01:00
Adrià Casajús
5f831d593a
CamelCase to snake_case 2022-03-21 17:59:43 +01:00
Son Nguyen Kim
a783b78a7f
Merge pull request #847 from simple-login/fix/replace-reverse-alias
decode, replace and encode for base64 encoding
2022-03-21 17:54:29 +01:00
Adrià Casajús
45459d65be
PR comments 2022-03-21 17:43:26 +01:00
Adrià Casajús
16275620ae
Also quarantine soft_fail dmarc results 2022-03-21 17:38:41 +01:00
Son
f554375f23 decode, replace and encode for base64 encoding 2022-03-21 17:29:22 +01:00
Son Nguyen Kim
7464588144
Merge pull request #845 from simple-login/feature/api-key-require-sudo
require password to use the api key page
2022-03-21 16:28:21 +01:00
Son
2baebe7934 remove unused import 2022-03-21 14:43:27 +01:00
Son
1952f368a8 require password to use the api key page 2022-03-21 14:40:47 +01:00
Son
9dc7cff87f add rate limiting for /auth/mfa 2022-03-21 14:23:35 +01:00
Son
a662ef4aee remove g.deduct_limit in api auth endpoint 2022-03-21 14:23:20 +01:00
Adrià Casajús
4d13e0c2b8
Rename 2022-03-21 12:32:50 +01:00
Adrià Casajús
35b47f4698
Updated test 2022-03-21 12:31:25 +01:00
Adrià Casajús
9930433d21
Use custom event 2022-03-21 12:14:51 +01:00
Adrià Casajús
06a1363e92
Updated MR comments 2022-03-21 12:03:11 +01:00
Adrià Casajús
cdea0f5ee2
Rename header 2022-03-21 10:43:19 +01:00
Adrià Casajús
d53ea381a0
Fix signature 2022-03-21 10:43:18 +01:00
Adrià Casajús
4a533bb03b
Fix imports 2022-03-21 10:43:18 +01:00
Adrià Casajús
44dd06fabf
Added spoofed email test 2022-03-21 10:43:18 +01:00
Adrià Casajús
c9cbaeb460
format 2022-03-21 10:43:17 +01:00
Adrià Casajús
e8013f8e0c
Initial parse of rpamd extra headers 2022-03-21 10:43:17 +01:00
Son
0931642d11 use 10.0.0.0 network instead of 240.0.0.0 2022-03-20 10:38:58 +01:00
Son
7f4357a329 log headers 2022-03-16 10:25:28 +01:00
Son
fa2f83dbf4 fix and refactor 2022-03-16 10:24:59 +01:00
Son
cd693eda69 avoid backscatter issue when unauthorized emails are sent to reverse alias 2022-03-16 09:06:49 +01:00
Son
93009158a8 fix 2022-03-16 09:05:57 +01:00
Son
7e0992b767 add mime version header for transactional email 2022-03-14 19:23:38 +01:00
Son Nguyen Kim
79154378f2
Merge pull request #836 from cquintana92/feature/allow-to-edit-manual-subscription
Allow to edit manual subscription
2022-03-14 18:07:07 +01:00
Son Nguyen Kim
6d52daee21
Merge pull request #835 from acasajus/new/admin-audit-trail
New/admin audit trail
2022-03-14 16:51:38 +01:00
Carlos Quintana
ed58e811d1
Allow to edit manual subscription 2022-03-14 16:47:30 +01:00
Adrià Casajús
479a7420cb
Useful time format 2022-03-14 15:40:50 +01:00
Adrià Casajús
b463ba8f41
Added filter 2022-03-14 15:33:09 +01:00
Adrià Casajús
bf177ac5ba
Remove unused 2022-03-14 15:29:17 +01:00
Adrià Casajús
9b16143e59
Show nicer admin logs 2022-03-14 15:28:53 +01:00
Adrià Casajús
553d8976be
Added extend subscription log 2022-03-14 15:07:51 +01:00
Adrià Casajús
b44904bc15
Update parent migration 2022-03-14 11:06:30 +01:00
Adrià Casajús
549c6ec7d3
Comment changes 2022-03-11 11:37:14 +01:00
Son
5127534a00 add more logging 2022-03-11 08:56:27 +01:00
Adrià Casajús
4368fd323f
Less changes 2022-03-10 18:13:33 +01:00
Adrià Casajús
d0860cd54d
Merge remote-tracking branch 'origin/master' into new/admin-audit-trail
* origin/master: (35 commits)
  reduce nb of commit
  show "more" only when a notification has a title. Show either title or message. Use bold font when a notification isn't read
  create a notification when an alias is disabled
  mark a notification as read when user arrives on the notification page
  Use plausible outbound link tracking
  add more log
  fix discover page
  fix
  fix "local variable 'alias_id' referenced before assignment"
  make sure to close session in monitoring
  use Date instead of date for header value
  lessen alias automatic disable check
  refactor
  return the block reason in should_disable()
  add adhoc upgrade on admin
  add extend subscription for 1 month to admin
  disable edition on admin
  comment out some admin pages
  fix migration
  fix duplicated stats
  ...
2022-03-10 18:10:13 +01:00
Adrià Casajús
733efc387c
Updated admin view 2022-03-10 17:49:30 +01:00
Adrià Casajús
98c942d84a
Added admin log view 2022-03-10 17:32:35 +01:00
Adrià Casajús
bc82bab1eb
Added alembic migration 2022-03-10 16:37:21 +01:00
Adrià Casajús
1d15af53b7
Add an audit log for the admin panel 2022-03-10 16:13:31 +01:00
Son Nguyen Kim
9807d32159
Merge pull request #834 from simple-login/feature/improve-notif
Improve notification
2022-03-10 08:34:29 +01:00
Son
ed12e47077 reduce nb of commit 2022-03-10 08:33:26 +01:00
Son
e0b5bd36a6 show "more" only when a notification has a title. Show either title or message. Use bold font when a notification isn't read 2022-03-09 17:59:42 +01:00
Son
fb00c18d5a create a notification when an alias is disabled 2022-03-09 17:59:02 +01:00
Son
0e3a5c3d3c mark a notification as read when user arrives on the notification page 2022-03-09 17:58:26 +01:00
Son Nguyen Kim
aa5c86605a
Merge pull request #833 from acasajus/new/outbound-tracking
Use plausible outbound link tracking
2022-03-09 10:36:01 +01:00
Adrià Casajús
b35b13b764
Use plausible outbound link tracking 2022-03-09 09:45:09 +01:00
Son
b6b917eba8 add more log 2022-03-08 18:35:18 +01:00
Son
6f80edfd64 fix discover page 2022-03-08 16:38:03 +01:00
Son
b711743d6e fix 2022-03-08 10:31:20 +01:00
Son
89218fab7f fix "local variable 'alias_id' referenced before assignment" 2022-03-08 10:30:29 +01:00
Son
ed089109bb make sure to close session in monitoring 2022-03-07 17:52:16 +01:00
Son
a64a70cbc8 use Date instead of date for header value 2022-03-07 15:57:29 +01:00
Son
350f498b94 lessen alias automatic disable check 2022-03-07 15:50:58 +01:00
Son
99dc45e09a refactor 2022-03-07 15:45:36 +01:00
Son
71136669e9 return the block reason in should_disable() 2022-03-07 15:44:27 +01:00
Son
f7ba3873d0 add adhoc upgrade on admin 2022-03-02 19:05:17 +01:00
Son
52a911f9d3 add extend subscription for 1 month to admin 2022-03-02 19:04:45 +01:00
Son
b2d8f5a017 disable edition on admin 2022-03-02 19:04:30 +01:00
Son
627b2e56d9 comment out some admin pages 2022-02-28 16:40:07 +01:00
Son
8502e1666b fix migration 2022-02-28 11:14:59 +01:00
Son
3d1a960702 fix duplicated stats 2022-02-28 10:43:30 +01:00
Son Nguyen Kim
6a520e110c
Merge pull request #816 from simple-login/feature/include-sender-in-header
Feature/include sender in header
2022-02-28 09:24:18 +01:00
Son Nguyen Kim
d4867dc524
Merge pull request #819 from simple-login/feature/optimize-query-time
optimize dashboard page: load custom domain using joinedload()
2022-02-28 09:23:52 +01:00
Son
205d8d7d3f add index for Alias custom_domain_id and directory_id columns 2022-02-26 17:51:50 +01:00
Son
4faf0d7636 optimize dashboard page: load custom domain using joinedload() instead of explicit join 2022-02-26 17:34:53 +01:00
Son
fa95f4273d ui tweak 2022-02-26 16:12:44 +01:00
Son
9c67aad34d remove "reply to this email" 2022-02-26 15:29:33 +01:00
Son Nguyen Kim
ad54c7ece0
Merge pull request #815 from acasajus/new/drag-drop-pgp
Allow drag and drop of keys into the text area
2022-02-25 15:56:52 +01:00
Adrià Casajús
c2ae38ec8f
typo 2022-02-25 15:01:17 +01:00
Adrià Casajús
61d1655529
Move all js to a source file 2022-02-25 14:58:38 +01:00
Son Nguyen Kim
7df93c2ee5
Merge pull request #813 from cquintana92/feature/make-nameservers-configurable
Make nameservers configurable
2022-02-25 12:29:50 +01:00
Son
6c8d4310e5 only set the X-SimpleLogin-Envelope-From header if user has this option enabled 2022-02-25 12:24:54 +01:00
Son
007aa56551 user can turn on/off the including sender in header option 2022-02-25 12:24:54 +01:00
Son
51598ada02 add User.include_header_email_header column 2022-02-25 12:24:54 +01:00
Carlos Quintana
e9dd73e99b
Replace env by os.environ.get 2022-02-25 11:19:49 +01:00
Son Nguyen Kim
4df32b3b03
Merge pull request #814 from acasajus/new/multiple-mx
Allow to have lower priority MX servers that do not belong to simplelogin
2022-02-25 09:30:59 +01:00
Adrià Casajús
3d498b4eae
Allow drag and drop of keys into the text area 2022-02-24 18:28:30 +01:00
Adrià Casajús
0c008edc82
Format 2022-02-24 17:30:07 +01:00
Adrià Casajús
77cf5d9620
Added tests 2022-02-24 17:25:48 +01:00
Adrià Casajús
01cc65bdca
Allow to have lower priority MX servers 2022-02-24 17:23:45 +01:00
Carlos Quintana
8f339923f8
Make nameservers configurable 2022-02-24 15:05:05 +01:00
Son
7da06ba424 return 422 if account not activated 2022-02-22 22:12:36 +01:00
Son Nguyen Kim
e9d134fe8f
Merge pull request #784 from FozzieHi/fix-testing-warnings
Fix deprecation warnings.
2022-02-21 17:12:36 +01:00
Son Nguyen Kim
e55c3a155b
Merge pull request #803 from acasajus/fix/sentry-APP-ZP
Only allow authenticated and enabled users to accept a OAuth post request
2022-02-21 17:11:53 +01:00
Adrià Casajús
4b13d5a28c
Fix test 2022-02-21 16:03:39 +01:00
Son
8fc5fd6d16 improve wording 2022-02-21 16:01:46 +01:00
Son Nguyen Kim
7d008228e3
Merge pull request #811 from cquintana92/feature/ignore-or-reject-for-blocked-contacts
Allow to configure ignore or reject response for blocked contacts
2022-02-21 15:55:48 +01:00
Son
f8640bfc91 change subscription cancel email 2022-02-21 15:10:22 +01:00
Son Nguyen Kim
bfcd75bdea
Merge pull request #801 from acasajus/new/no-reply
Send support questions to the support ticket page
2022-02-21 15:07:27 +01:00
Carlos Quintana
ee9170bb17
Allow to configure ignore or reject response for blocked contacts 2022-02-21 12:52:21 +01:00
Adrià Casajús
33163660f7
PR comments 2022-02-21 12:30:26 +01:00
Adrià Casajús
3e983e3557
Only allow authenticated and enabled users to accept a OAuth post request 2022-02-17 17:25:04 +01:00
Adrià Casajús
b069f81920
Reply only once per user even if they send it from any mailbox 2022-02-17 14:33:04 +01:00
Adrià Casajús
b0ac2f871a
Fixes 2022-02-17 13:21:40 +01:00
Adrià Casajús
398c1a55f1
Change SUPPORT_EMAIL to NOREPLY 2022-02-17 13:18:52 +01:00
Adrià Casajús
780f5b75aa
Fixed PR comments 2022-02-17 13:16:11 +01:00
Son Nguyen Kim
be161d0778
Merge pull request #797 from cquintana92/feature/only-allow-relative-redirects
Only allow relative and controlled redirects
2022-02-17 11:23:00 +01:00
Adrià Casajús
0dfbe1bca4
Add footers to html and txt templates 2022-02-16 18:52:35 +01:00
Adrià Casajús
17c6923ddc
Add missing template 2022-02-16 18:39:32 +01:00
Adrià Casajús
1b525a55a5
Add debug message 2022-02-16 18:39:18 +01:00
Adrià Casajús
15ce7b00d8
Reply to noreply@... once per user 2022-02-16 18:38:31 +01:00
Carlos Quintana
2a751624a8
Default ALLOWED_REDIRECT_DOMAINS to URL if it's not set 2022-02-16 16:16:14 +01:00
Carlos Quintana
b4e291d4fd
Make NextUrlSanitizer a static class 2022-02-16 16:05:50 +01:00
Carlos Quintana
6be99bc576
Do not account for urlencoded redirects 2022-02-16 16:02:13 +01:00
Carlos Quintana
a44acf1846
Add support for allowed redirect domains 2022-02-16 09:38:55 +01:00
Son Nguyen Kim
88ed4b8d2b
Merge pull request #800 from acasajus/fix/support-new-alias-warning
Show a warning if the user cannot create more aliases
2022-02-15 19:36:32 +01:00
Adrià Casajús
92ec70c497
Show a warning if the user cannot create more aliases 2022-02-15 18:51:13 +01:00
Carlos Quintana
39222cf868
Simplify conditional 2022-02-15 16:33:30 +01:00
Carlos Quintana
2f9489fe39
Only allow relative redirects 2022-02-15 15:16:31 +01:00
Son
c947e7cbd5 improve cron job 2022-02-15 09:36:37 +01:00
Son
4d23134372 only show ZENDESK button if ZENDESK_ENABLED 2022-02-14 18:09:26 +01:00
Son
728d935d65 add ZENDESK_ENABLED param 2022-02-14 18:08:32 +01:00
Son
1e7d224f35 mention about using pycharm to reformat html code 2022-02-14 18:06:41 +01:00
Son
bef3b8bd96 IDE reformat header.html 2022-02-14 18:05:32 +01:00
Son
c3cd1419f9 reformat code: put POST handling on top 2022-02-14 18:02:54 +01:00
Son
a0bb4e9ccc more verbose error 2022-02-14 18:02:30 +01:00
Son
473d0350ca consistent styling 2022-02-14 18:02:09 +01:00
Son
5c0bfe2f34 remove unneeded style 2022-02-14 17:55:27 +01:00
Son
ea00e2ba8f move script block to the end 2022-02-14 17:54:28 +01:00
Son
634ad4ac19 IDE reformatting 2022-02-14 17:54:04 +01:00
Son Nguyen Kim
69c8980c18
Merge pull request #792 from acasajus/new/zendesk-support
Create support tickets via zendesk
2022-02-14 17:53:30 +01:00
Adrià Casajús
d24ee42240
cosmetics 2022-02-14 16:00:00 +01:00
Adrià Casajús
416e7b363a
PR fixes 2022-02-14 15:58:36 +01:00
Adrià Casajús
305ce38379
PR changes 2022-02-14 11:19:03 +01:00
Son Nguyen Kim
f2d02e6f93
Merge pull request #795 from simple-login/dependabot/pip/protobuf-3.15.0
Bump protobuf from 3.13.0 to 3.15.0
2022-02-12 19:35:35 +01:00
Adrià Casajús
700856053a
PR comment fixes 2022-02-11 13:32:31 +01:00
dependabot[bot]
c8ca51fc5e
Bump protobuf from 3.13.0 to 3.15.0
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 3.13.0 to 3.15.0.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.13.0...v3.15.0)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-11 03:45:30 +00:00
Adrià Casajús
8120128a51
Added Zendesk token 2022-02-10 12:59:48 +01:00
Adrià Casajús
639d4412e1
Updated comments from PR 2022-02-10 12:47:31 +01:00
Adrià Casajús
c9974d5321
Removed successful ticket created page and replaced with notification 2022-02-10 12:38:56 +01:00
Adrià Casajús
3fedc84c95
Add rate limit to ticket createion 2022-02-10 12:34:46 +01:00
Son Nguyen Kim
c18f9658b0
Merge pull request #787 from FozzieHi/test-config
Update testing suite and refactor.
2022-02-10 11:37:36 +01:00
Adrià Casajús
e844c9a392
Removed disabled page and redirected to the normal dashboard 2022-02-10 11:04:36 +01:00
Son Nguyen Kim
5121a3c2d9
Merge pull request #793 from jeifour/patch-1
Add required package for authentication with AWS SES
2022-02-10 10:51:54 +01:00
Son
fbf3d49717 run the growth stats at different time than the daily monitoring stats 2022-02-10 10:39:03 +01:00
Adrià Casajús
f59c5499fb
Formatting 2022-02-10 10:30:28 +01:00
Jakob Yanagibashi
01bf037638
Update ses.md
SASL package
2022-02-09 17:54:13 +01:00
Adrià Casajús
8aee883aae
Updated with more PR comments 2022-02-09 16:41:04 +01:00
Adrià Casajús
95fa95649d
Added comments from PR 2022-02-09 16:20:55 +01:00
Adrià Casajús
e57dcac2d2
Added zendesk submission flow 2022-02-09 12:00:48 +01:00
Adrià Casajús
219d5b998f
Add a suport form to create tickets in zendesk 2022-02-08 22:04:25 +01:00
Son
5b62f5a745 add rate limit to /auth/register 2022-02-07 18:45:41 +01:00
george
bbb2ac64b9
Use select instead of extend-select. 2022-02-06 23:55:27 +00:00
george
68462f2d8f
Reformat. 2022-02-06 23:17:52 +00:00
george
83434c3212
Use extend-select over select. 2022-02-06 21:36:37 +00:00
george
813e83d673
Use extend-select over select. 2022-02-06 21:34:36 +00:00
george
05e1208d57
Add more flake8-bugbear lints. 2022-02-06 20:59:29 +00:00
george
c415324932
Add flake8-bugbear 2022-02-06 20:37:43 +00:00
george
9999f7de1e
Update .flake8 for black compatibility. 2022-02-06 20:16:21 +00:00
george
17e7635dab
Update pytest & pytest-cov. 2022-02-06 16:36:25 +00:00
george
e68363dbbc
Set pytest testpaths. 2022-02-06 16:19:14 +00:00
george
7f765e83b7
Use install-poetry to use poetry with caching. 2022-02-06 16:08:40 +00:00
george
4800274b33
Run flake8 through poetry. 2022-02-06 14:49:16 +00:00
george
116fc7114a
Update test_can_be_used_as_personal_email to only skip if using GitHub Actions. 2022-02-06 14:37:46 +00:00
george
f7be992437
Update black, flake8 and pre-commit and use specific pre-commit versions. 2022-02-06 14:25:53 +00:00
george
87a327912f
Delete .python-version. 2022-02-05 14:57:02 +00:00
george
42b9471a8f
Add .python-version to .gitignore so virtualenv names are not added. 2022-02-05 14:44:40 +00:00
Son
cca23b753c Extract daily monitoring report from stats() 2022-02-04 15:45:21 +01:00
Son
5da31f53b4 add MONITORING_EMAIL param 2022-02-04 15:43:40 +01:00
george
936d90a5f5
Fix deprecation warnings. 2022-02-04 13:49:38 +00:00
Son Nguyen Kim
68acfc986a
Merge pull request #782 from FozzieHi/unit-tests
Improve and refactor testing.
2022-02-04 10:51:44 +01:00
Son Nguyen Kim
2d980b8990
Merge pull request #780 from FozzieHi/api-key-tests
Add unit tests for API keys.
2022-02-04 10:46:35 +01:00
george
e6276dc32e
Fix typo. 2022-02-03 21:44:27 +00:00
george
172e509f53
Add comment. 2022-02-03 21:43:40 +00:00
george
5815ee0b2e
Match sleep with new-migration script. 2022-02-03 21:11:42 +00:00
george
3a5f077bbf
Improve and refactor testing. 2022-02-03 21:08:38 +00:00
george
3837a9955e
Just get the API key by user ID. 2022-02-03 19:49:29 +00:00
george
836e599517
Use POST requests to create and delete through the dashboard. 2022-02-03 19:47:41 +00:00
george
010c343641
Refactor to the dashboard folder. 2022-02-03 19:33:40 +00:00
george
709ccb176a
Test the dashboard POST request instead of directly testing the API. 2022-02-03 19:30:10 +00:00
george
c0712a6b95
Login using the test utils method. 2022-02-03 19:20:39 +00:00
Son Nguyen Kim
a436859a55
Merge pull request #723 from simple-login/snyk-upgrade-3c2f300d18742ee8000234e4d3675193
[Snyk] Upgrade htmx.org from 1.6.0 to 1.6.1
2022-02-03 20:11:06 +01:00
Son Nguyen Kim
72a3e118c8
Merge pull request #776 from simple-login/dependabot/pip/cryptography-3.3.2
Bump cryptography from 3.1.1 to 3.3.2
2022-02-03 20:10:41 +01:00
Son Nguyen Kim
ed02438c10
Merge pull request #774 from simple-login/dependabot/pip/jinja2-2.11.3
Bump jinja2 from 2.11.2 to 2.11.3
2022-02-03 20:10:30 +01:00
Son Nguyen Kim
eee8a5bf97
Merge pull request #773 from simple-login/dependabot/pip/pyyaml-5.4
Bump pyyaml from 5.3.1 to 5.4
2022-02-03 20:10:22 +01:00
Son Nguyen Kim
377e94b883
Merge pull request #778 from simple-login/dependabot/pip/ipython-7.31.1
Bump ipython from 7.18.1 to 7.31.1
2022-02-03 20:10:00 +01:00
Son Nguyen Kim
becb0b50bb
Merge pull request #779 from simple-login/dependabot/pip/py-1.10.0
Bump py from 1.9.0 to 1.10.0
2022-02-03 20:09:51 +01:00
Son Nguyen Kim
4c9ae778e7
Merge pull request #772 from simple-login/dependabot/pip/pygments-2.7.4
Bump pygments from 2.7.1 to 2.7.4
2022-02-03 20:09:29 +01:00
george
17353c306c
Reduce session commits. 2022-02-03 15:28:56 +00:00
george
8f8a8b875b
Add multiple API keys for test user 1. 2022-02-03 15:05:46 +00:00
george
1f04dfad61
Add unit tests for API keys. 2022-02-03 15:02:32 +00:00
dependabot[bot]
1a74269ff1
Bump py from 1.9.0 to 1.10.0
Bumps [py](https://github.com/pytest-dev/py) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/pytest-dev/py/releases)
- [Changelog](https://github.com/pytest-dev/py/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/py/compare/1.9.0...1.10.0)

---
updated-dependencies:
- dependency-name: py
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-03 10:30:49 +00:00
dependabot[bot]
429fc3ae51
Bump ipython from 7.18.1 to 7.31.1
Bumps [ipython](https://github.com/ipython/ipython) from 7.18.1 to 7.31.1.
- [Release notes](https://github.com/ipython/ipython/releases)
- [Commits](https://github.com/ipython/ipython/compare/7.18.1...7.31.1)

---
updated-dependencies:
- dependency-name: ipython
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-03 10:30:44 +00:00
dependabot[bot]
2e896e28e1
Bump cryptography from 3.1.1 to 3.3.2
Bumps [cryptography](https://github.com/pyca/cryptography) from 3.1.1 to 3.3.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.1.1...3.3.2)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-03 10:29:53 +00:00
dependabot[bot]
7caffc0a4f
Bump jinja2 from 2.11.2 to 2.11.3
Bumps [jinja2](https://github.com/pallets/jinja) from 2.11.2 to 2.11.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/2.11.2...2.11.3)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-03 10:28:27 +00:00
dependabot[bot]
d8f246c3e2
Bump pyyaml from 5.3.1 to 5.4
Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.3.1 to 5.4.
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/5.3.1...5.4)

---
updated-dependencies:
- dependency-name: pyyaml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-03 10:28:13 +00:00
dependabot[bot]
d6af7e8362
Bump pygments from 2.7.1 to 2.7.4
Bumps [pygments](https://github.com/pygments/pygments) from 2.7.1 to 2.7.4.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.7.1...2.7.4)

---
updated-dependencies:
- dependency-name: pygments
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-03 10:28:00 +00:00
Son
b490acead8 small fix 2022-02-03 11:18:10 +01:00
Son
d570868dcf Revert "remove analytics"
This reverts commit ac2ee4f2d0.
2022-02-03 11:17:13 +01:00
Son
4d1c4cfdff support pinned parameter in /api/v2/aliases 2022-02-03 11:16:49 +01:00
Son
70cb0609d8 refactor 2022-02-03 11:16:49 +01:00
Son
730cc14cca install deprecated package 2022-02-03 11:16:49 +01:00
Son Nguyen Kim
543923b325
Merge pull request #760 from FozzieHi/delete-all-api-keys-button
Add a button to delete all API Keys
2022-02-02 18:31:05 +01:00
Son
043d62bf20 redirect users to bounce emails page instead in notification message 2022-02-02 16:52:04 +01:00
Son
049bd746ad refactor shell 2022-01-26 15:22:37 +01:00
Son
5a712f3877 make sure subdomain can only contain lowercase letters, numbers and dashes. 2022-01-26 14:53:27 +01:00
george
285c1d10cf
Move Delete All button to below the list of current API Keys. 2022-01-25 18:36:13 +00:00
george
74713c2142
Rename method. 2022-01-25 18:32:34 +00:00
Son Nguyen Kim
89a800eed9
Merge pull request #753 from FozzieHi/totp-invalid-login-email
Invalid TOTP and recovery code email notifications
2022-01-24 18:35:52 +01:00
Son
f1c0b94ffd fix run test script: sleep to make sure the Db container is ready 2022-01-24 18:05:17 +01:00
Son Nguyen Kim
227087a10f
Merge pull request #751 from mrbluecoat/master
minor installation instruction fixes
2022-01-24 18:01:34 +01:00
Son
3be4f341a2 fix reply phase template 2022-01-24 16:51:27 +01:00
Son
fc3f06f4d8 create notification listing page 2022-01-24 16:45:36 +01:00
Son
78c14fa67e create notification for bounce email during reply phase 2022-01-24 16:13:45 +01:00
Son
90fa4abf69 create a notification for a bounce email 2022-01-24 16:10:36 +01:00
Son
bdb97e73e9 display notification title, "more" button to the notification page 2022-01-24 15:22:26 +01:00
Son
1de6fefc59 add notification detail page 2022-01-24 15:22:01 +01:00
Son
5b7949f346 return title in /api/notifications 2022-01-24 15:20:59 +01:00
Son
3422f038eb add Notification title 2022-01-24 15:18:56 +01:00
george
65531b5c63
Add a button to delete all API Keys. 2022-01-23 18:38:54 +00:00
Son
e73288354d remove IGNORED_EMAILS variable 2022-01-21 19:30:27 +01:00
george
ab72927a16
Update text. 2022-01-20 18:24:28 +00:00
george
50122da0fe
Implement API notifications and use a function in email_utils 2022-01-20 17:42:11 +00:00
george
42407a0543
Send the email after the local error. 2022-01-20 16:44:15 +00:00
george
f7f91afc1e
Send a notification email for invalid recovery codes. 2022-01-20 16:41:42 +00:00
george
6b4d276ffe
Add change password button with link to dashboard. 2022-01-20 15:40:28 +00:00
george
6d736aa915
Implement rate limiting with send_email_with_rate_control. 2022-01-20 15:05:18 +00:00
george
122a402c22
Clarify text. 2022-01-20 14:23:19 +00:00
george
0eb2984b9c
Add invalid TOTP login email notifications. 2022-01-20 14:18:47 +00:00
Mr. Blue Coat
99ff4c6f88
restore pwd paths 2022-01-19 13:35:33 -07:00
Son
b929dc5462 check if alias is not none 2022-01-18 09:40:50 +01:00
Mr. Blue Coat
724edee311
link update 2022-01-17 08:19:00 -07:00
Son
efdb0a60d3 no need to raise error if email processing takes more than 60s 2022-01-17 14:42:27 +01:00
Son
94ecdb0515 replace patreon by open collective 2022-01-17 10:47:16 +01:00
Mr. Blue Coat
11fe7f6f65
fix for server restart survivability 2022-01-16 18:57:56 -07:00
Mr. Blue Coat
1caf8e5dcd
minor installation instruction fixes
Closes #749
2022-01-16 16:16:56 -07:00
Son
0806f9243e return custom domain json in patch 2022-01-16 17:26:11 +01:00
Son
8ff3b5ef8e ignore VERPForward error 2022-01-16 11:52:44 +01:00
Son
0e496518ba handle case alias is deleted in handle_hotmail_complaint 2022-01-16 11:47:50 +01:00
Son
1c8a0c4f16 remove patreon 2022-01-14 11:59:11 +01:00
Son
a72f1bd414 remove paypal 2022-01-14 11:58:14 +01:00
Son
ca18c9c5e0 remove unused import 2022-01-13 16:40:07 +01:00
Son
e73a46cf36 fix init_app 2022-01-13 16:34:22 +01:00
Son
ca971567c5 use newrelic in monitoring. Monitor the number of db connection 2022-01-13 10:39:22 +01:00
Son
841621dbe2 handle the case mailboxes is empty in try_auto_create_via_domain 2022-01-13 09:33:32 +01:00
Son
4cea47cc27 add setting for include_website_in_one_click_alias 2022-01-12 11:50:49 +01:00
Son
6cd8e45d21 return the default sender format (AT) in case user uses a non-supported sender format 2022-01-12 10:19:25 +01:00
Son
db24ed8739 remove unused import 2022-01-11 13:23:44 +01:00
Son
2a1ef7beec notify user every time a reply can't be sent 2022-01-11 13:14:47 +01:00
Son
42a29eba90 remove \r or \n from headers before processing 2022-01-11 13:11:28 +01:00
Son
5e7ff7a694 fix logging 2022-01-11 12:29:42 +01:00
Son
2f6229cd54 change collapse button display "more" -> "less" 2022-01-11 12:25:49 +01:00
Son
c0067b7657 add more log 2022-01-10 15:58:22 +01:00
Son
94bbade62e refactor 2022-01-10 15:05:15 +01:00
Son
73d781cf6b handle non number alias_id 2022-01-10 10:44:41 +01:00
Son
9c696bd038 ignore VERPReply 2022-01-09 20:35:57 +01:00
Son
6cdf5637aa validate the alias address before creating 2022-01-09 20:22:41 +01:00
Son
a0727435eb use warning level 2022-01-09 20:13:41 +01:00
Son
f855d27836 delete EmailLog if pgp issue 2022-01-08 16:59:32 +01:00
Son
c5e4dd6d16 save email for debug with error name as prefix 2022-01-08 16:58:23 +01:00
Son
4298fe73e6 use warning level 2022-01-08 00:43:49 +01:00
Son
862d0e7a11 warn users if SL is used with another forwarding service 2022-01-08 00:42:03 +01:00
Son
ed4acebdb1 delete the email log in reply phase if NonReverseAliasInReplyPhase 2022-01-08 00:28:26 +01:00
Son
b23f9fa971 delete email_log if CannotCreateContactForReverseAlias 2022-01-08 00:23:10 +01:00
Son
01ba5e8bf0 return 5** if CannotCreateContactForReverseAlias 2022-01-08 00:16:16 +01:00
Son
ed39d47e7a log "Custom/nb_rcpt_tos" metric 2022-01-08 00:11:16 +01:00
Son
20b6ce29fc take into account authorized_address when checking email loop 2022-01-08 00:09:45 +01:00
Son
d8627fea97 handle case when non reverse-alias is present in the reply phase 2022-01-07 17:53:06 +01:00
Son
d4e31257fa make sure to output exception name in log 2022-01-07 16:45:12 +01:00
Son
ad185ebc3d comment out the ignore-loop-email setting 2022-01-07 16:22:52 +01:00
Son
2a1d735800 always ignore loop email 2022-01-07 16:22:35 +01:00
Son
fb87225d2d raise error when receiving emails sent from reverse alias 2022-01-07 16:14:21 +01:00
Son
746dfae495 remove unused import 2022-01-07 15:47:54 +01:00
Son
d4e1aec875 refactor 2022-01-07 14:57:47 +01:00
Son
6b31b8926e fix comment 2022-01-07 14:27:53 +01:00
Son
bf75f8e8ab add more logging 2022-01-07 14:26:58 +01:00
Son
40b6fde2c3 log more 2022-01-07 13:02:16 +01:00
Son
12a7e9b3fa refactor 2022-01-07 12:24:14 +01:00
Son
4fae291251 improve logging 2022-01-07 12:19:51 +01:00
Son
4c63b4c0f1 refactor 2022-01-07 12:18:46 +01:00
Son
1bdae7fbe8 handle CannotCreateContactForReverseAlias when user creates a new contact 2022-01-07 10:47:36 +01:00
Son
5195c9de8b raise error when a non reverse-alias is used during the reply phase 2022-01-07 10:34:08 +01:00
Son
035d238c75 do not delete DATE header 2022-01-07 10:22:46 +01:00
Son
db30639380 set Contact.automatic_created during the forward phase 2022-01-07 10:22:02 +01:00
Son
84d1f22a7b add Contact.automatic_created column to know which contact is created during the forward phase 2022-01-07 10:21:31 +01:00
Son
ad622df071 make sure a contact with website_email=reverse alias of another contact can't be created 2022-01-07 10:04:12 +01:00
Son
bb6aec8b80 fix out of office handling sent by contact 2022-01-06 19:34:17 +01:00
Son
723d871550 add more info to "cannot handle email sent to reply VERP" log 2022-01-06 19:23:13 +01:00
Son
b306abb689 use yield_per_query() in cron whenever possible 2022-01-06 18:52:14 +01:00
Son
b2e4578953 add yield_per_query() helper 2022-01-06 18:50:54 +01:00
Son
01cc9fe388 optimize migrate_domain_trash: bulk create and delete, keep track of progress 2022-01-06 18:30:56 +01:00
Son
d1b9fb8bb5 add type annotation for Session 2022-01-06 18:30:14 +01:00
Son
17e9798bfd do not use error level in migrate_domain_trash 2022-01-06 15:36:43 +01:00
Son
37bb7655d5 remove unused import 2022-01-06 15:30:33 +01:00
Son
9ff323c746 make sure to set custom_domain_id when creating a new alias 2022-01-06 15:29:37 +01:00
Son
b7e8324e5a move get_custom_domain() to alias_utils 2022-01-06 15:20:09 +01:00
Son
55e3203512 refactor. Remove sleep when checking mailbox domain. 2022-01-06 15:13:59 +01:00
Son
33bd7dbcd6 refactor 2022-01-06 14:57:01 +01:00
Son
e7c473c943 add more logging info 2022-01-06 14:36:10 +01:00
Son
de9f994fe2 check suffix in try_auto_create 2022-01-06 11:12:26 +01:00
Son
3fb6dd4aeb check if there's an email that starts with "\u200f" (right-to-left mark (RLM)) in cron 2022-01-06 11:07:50 +01:00
Son
4976f48944 add /phone/provider2/sms 2022-01-05 18:14:51 +01:00
Son
b505ceebe9 add PHONE_PROVIDER_2_HEADER, PHONE_PROVIDER_2_SECRET config 2022-01-05 18:14:14 +01:00
Son
0c25ed939f fix logging 2022-01-05 17:43:11 +01:00
Son
778c90a164 tweak logging 2022-01-05 16:26:31 +01:00
Son
385dd1e755 handle out-of-office email in addition to bounce 2022-01-05 15:30:44 +01:00
Son
6c42872440 add is_bounce() 2022-01-05 15:22:22 +01:00
Son
ffc621596a fix is_automatic_out_of_office: only use "Auto-Submitted" header 2022-01-05 15:21:54 +01:00
Son
0abfb82fd1 investigate emails sent to reverse alias from <> 2022-01-05 15:20:17 +01:00
Son
6cb55e27f3 make sure alias that starts with bounce prefix can't be automatically created 2022-01-05 15:16:04 +01:00
Son
de23828df1 convert out-of-office email into normal email 2022-01-05 09:50:58 +01:00
Son
5e2ea81a6c do not consider out-of-office as bounce 2022-01-04 18:06:08 +01:00
Son
2ed7c5fcdb only return active number 2022-01-04 16:26:38 +01:00
Son
4ac8da1e8f poll messages on the phone reservation page 2022-01-04 16:24:50 +01:00
Son
4d8c89105f GET /api/phone/reservations/:reservation_id 2022-01-04 16:22:41 +01:00
Son
d51c32ad51 always return 200 for phone provider1 callback 2022-01-04 14:58:41 +01:00
Son
72bd998b9b fix logging 2022-01-04 14:58:24 +01:00
Son
bc2f9ad45f add PhoneNumber.comment column 2022-01-04 14:54:55 +01:00
Son
127f8daad7 add /phone/provider1/sms 2022-01-04 14:53:22 +01:00
Son
3484f71dac add PHONE_PROVIDER_1_HEADER and PHONE_PROVIDER_1_SECRET config 2022-01-04 14:52:56 +01:00
Son
a4b113b7fa remove DISPOSABLE_FILE_PATH 2022-01-04 09:40:10 +01:00
Son
1b5f059899 only show (past) reservations if needed 2022-01-04 09:37:49 +01:00
Son
d38fa95eed make sure number of minutes is specified 2022-01-04 09:35:34 +01:00
Son
1149fe964b handle case where domain is already added in invalid mailbox domain 2022-01-03 17:09:40 +01:00
Son
325207d6ba Use InvalidMailboxDomain instead of DISPOSABLE_EMAIL_DOMAINS 2022-01-03 10:33:21 +01:00
Son
4332fd3244 Create InvalidMailboxDomain model 2022-01-03 10:31:33 +01:00
Son
414f6a2463 handle case a custom domain is deleted in the meantime 2022-01-03 10:07:41 +01:00
Son
f548e74e77 refactor 2022-01-03 10:06:56 +01:00
Son
85fb859dcb show error when a domain can't be used as random alias default domain 2022-01-03 10:05:06 +01:00
Son
08c7aa8b98 ignore smtp errors for onboarding emails 2022-01-03 10:01:56 +01:00
Son
34118f459a ignore smtp error in send_trial_end_soon_email() 2022-01-03 09:59:52 +01:00
Son
558200113c remove unnecessary code in send_email() 2022-01-01 11:47:45 +01:00
Son
b24d58bdf3 handle case alias was deleted in the meantime 2021-12-31 12:14:22 +01:00
Son
01a8a0343e log when an alias is deleted 2021-12-31 11:18:08 +01:00
Son
61226545c2 handle the 2 consecutive dots in alias 2021-12-31 11:15:24 +01:00
Son
e3d06f7a1d disable the sqlalchemy debug panels locally 2021-12-31 11:10:46 +01:00
Son
9ee449722a new domain has ownership verified if its root has the ownership verified 2021-12-31 11:10:36 +01:00
Son
a6f5b755aa set apple_sub.product_id 2021-12-30 16:20:31 +01:00
Son
e1d82b7e0d Add AppleSubscription.product_id col 2021-12-30 16:20:18 +01:00
Son
30ba566457 take into account _MACAPP_MONTHLY_PRODUCT_ID in verify_receipt 2021-12-30 16:15:33 +01:00
Son
22cf8cfe38 send Custom/smtp_connection_time to newrelic 2021-12-30 14:17:46 +01:00
Son
2cd50c582a remove NEWRELIC_CONFIG_PATH 2021-12-30 14:15:49 +01:00
Son
77f1544a1d remove newrelic init from email handler 2021-12-30 14:15:49 +01:00
Son
c5185eddf3 Revert "log "Custom/smtp_connection_time" metric in newrelic"
This reverts commit 378bad6253.
2021-12-30 11:55:48 +01:00
Son
378bad6253 log "Custom/smtp_connection_time" metric in newrelic 2021-12-30 11:46:01 +01:00
Son
a64968f6e5 consider utf-8 email encoding as no encoding 2021-12-30 11:36:52 +01:00
Son
b3469ba9d4 log how much time to get a smtp connection 2021-12-30 11:28:50 +01:00
Son
c2e95f0853 reformat email_handler 2021-12-30 10:24:57 +01:00
Son
c14a7b4f7a use with ... for smtp 2021-12-29 17:09:24 +01:00
Son
d80ecfb068 use error log if email processing takes more than 1 minute 2021-12-29 16:30:12 +01:00
Son
f439e39580 cache smtp server and remove POSTFIX_PORT_FORWARD 2021-12-29 16:26:37 +01:00
Son
9e019ae98a retry sending mail if TimeoutError 2021-12-29 15:17:57 +01:00
Son
d8f7323b95 remove unused import 2021-12-29 11:24:12 +01:00
Son
8530abfb2d reduce log level for emails sent from a reverse alias 2021-12-29 11:18:54 +01:00
Son
733a9c42b0 delete activation code before sending email to avoid any delay 2021-12-29 10:26:42 +01:00
Son
80b0af91e5 allow user to create alias with domain that has ownership verified (and might not have MX verified) 2021-12-29 10:24:22 +01:00
Son
335a89f912 enable email sending retry in job runner 2021-12-28 16:43:26 +01:00
Son
b9e2a79933 enable email sending retry in cron job 2021-12-28 16:42:01 +01:00
Son
e752e466e1 reformat api.md 2021-12-28 15:25:06 +01:00
Son
a270c72d60 add random_alias_suffix to settings api 2021-12-28 15:24:58 +01:00
Son
229dc7fd44 fix test 2021-12-28 15:09:33 +01:00
Son
83be94b43e user can create aliases for domains that don't have mx verified 2021-12-28 12:01:32 +01:00
Son
bd614278df increase bounce info length 2021-12-28 10:51:43 +01:00
Son
fc42db43ca add new sender formats: NAME_ONLY, AT_ONLY, NO_NAME 2021-12-28 10:49:37 +01:00
Son
922fa4925e handle ObjectDeletedError 2021-12-28 10:21:26 +01:00
Son
24a392818b sl_sendmail tries by default 2 times before giving up: replace can_retry by retries 2021-12-27 17:03:44 +01:00
Son
2cf1c4143a reduce sleep time between sl_sendmail failure 2021-12-27 17:00:11 +01:00
Son
32fffeaa6e handle case bounce info too verbose in daily report 2021-12-26 22:25:00 +01:00
Son
a6569d47dd do not put price in plan_name to take into account discount 2021-12-26 22:04:45 +01:00
Son
f0e582c1a6 use postfix retry for SMTPServerDisconnected (in addition to SMTPRecipientsRefused) error 2021-12-23 19:34:17 +01:00
Son
584772f798 fix upgrade_channel 2021-12-23 19:28:57 +01:00
Son
879b364a47 return 421 to retry when SMTPRecipientsRefused error 2021-12-23 18:17:29 +01:00
Son
544df7034d do not retry sending email in send_email() 2021-12-16 21:31:01 +01:00
Son
75d6b1dab5 add more logging 2021-12-16 15:09:15 +01:00
Son
64c6ef2cbe retry when SMTPRecipientsRefused too 2021-12-16 15:06:26 +01:00
Son
a142a430d2 use sl_sendmail instead of smtp.sendmail 2021-12-16 10:32:10 +01:00
Son
eec2880c41 fix case signed_suffix is None 2021-12-15 17:12:27 +01:00
Son
79ca39a625 use regex_match instead of re.fullmatch() 2021-12-14 15:00:32 +01:00
Son
5e7730c35c refactor: move regex_match() to its own file 2021-12-14 15:00:32 +01:00
Son Nguyen Kim
0d7d451313
Merge pull request #724 from zouma83/patch-1
Create gmail-smtp-relay.md
2021-12-14 10:49:40 +01:00
Florent Marquez
3997269670
Create gmail-relay.md
using Gmail as SMTP relay to send email from SimpleLogin on port 587
2021-12-13 21:05:19 +01:00
snyk-bot
4f84c0d1c9
fix: upgrade htmx.org from 1.6.0 to 1.6.1
Snyk has created this PR to upgrade htmx.org from 1.6.0 to 1.6.1.

See this package in npm:
https://www.npmjs.com/package/htmx.org

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-12-13 17:25:40 +00:00
Son
f3e8fc10a9 use re instead of re2 if error "Argument 'pattern' has incorrect type (expected bytes, got PythonRePattern)" 2021-12-11 19:38:22 +01:00
Son
a021bba811 fix toggle contact should only be used by authenticated user 2021-12-06 18:39:12 +01:00
Son
40299cbf34 improve auto create page wording 2021-12-06 18:21:17 +01:00
Son
c6a2af3c3c remove unused admin column 2021-12-06 18:04:23 +01:00
Son
c878e07c78 add warning message about subdomain quota when creating a new subdomain 2021-12-02 18:13:17 +01:00
Son
3e2c120a73 phone reservation page
- add twilio lib
- create phone listing, reservation page
- add twilio callback to receive messages
2021-12-02 17:03:13 +01:00
Son
7109dc7120 add models for phone: Country, Number, Reservation, Message 2021-12-02 16:50:26 +01:00
Son
daca70f2b4 add TWILIO_AUTH_TOKEN config 2021-12-02 16:30:30 +01:00
Son
226ce9333c return error if invalid alias address 2021-12-02 16:17:41 +01:00
Son
e1123961cf check if user has lifetime license on pricing page 2021-12-01 17:41:20 +01:00
Son
57ec92ed7c suggest user to use an email alias during the Paddle checkout 2021-12-01 17:27:29 +01:00
Son
4f9bb59b58 don't notify user who already have an non-canceled Paddle subscription 2021-12-01 17:18:19 +01:00
Son
4d388a202c allow user with manual or coinbase subscription to switch to paddle 2021-12-01 17:16:01 +01:00
Son
5dab819ac3 remove BlackFriday mention 2021-12-01 17:06:16 +01:00
Son
c6f49821c7 remove unused import 2021-12-01 10:39:08 +01:00
Son
34509cbbb3 delete account is protected by password 2021-12-01 10:11:18 +01:00
Son
4ffa5c9345 display expires date 2021-11-30 10:33:31 +01:00
Son
94a90665ea black format 2021-11-29 16:46:03 +01:00
Son
47f37fae25 Display multiple payment channels 2021-11-29 16:40:13 +01:00
Son
5dbc42a6a7 improve wording to explain what happens if user re-subscribe 2021-11-29 16:38:38 +01:00
Son
57b390733d do not show paid option for lifetime user 2021-11-29 16:27:25 +01:00
Son
20dfcfb88c do not show subscription end on header 2021-11-29 16:27:13 +01:00
Son
21bd4ed97e display all user payment channels on admin 2021-11-29 16:26:41 +01:00
Son
c29b5100fb delete User.subscription_cancelled 2021-11-29 16:17:24 +01:00
Son
325a1a9524 remove unused User.premium_end 2021-11-29 11:35:28 +01:00
Son
d875b2e0e5 do not send renewal reminder for lifetime user 2021-11-28 12:35:03 +01:00
Son
5f47d172e0 move fake_data() to dedicated fiel 2021-11-28 11:51:31 +01:00
Son
5ea087e7a3 handle paddle payment_refunded request 2021-11-28 10:39:04 +01:00
Son
c0c490517a Create /paddle_coupon to handle coupon purchase 2021-11-26 18:34:04 +01:00
Son
c356c75494 show coupon expires date when user buys a coupon 2021-11-26 18:20:46 +01:00
Son
4d0f6811b2 error if coupon is expired 2021-11-26 18:11:03 +01:00
Son
06d459ba99 Add coupon.expires_date column 2021-11-26 18:10:23 +01:00
Son
6d1b6720cf set content_type for plain text message 2021-11-25 10:34:59 +01:00
Son
dd6e265aa0 add RSPAMD_SIGN_DKIM and add "X-SimpleLogin-Want-Signing" header 2021-11-25 10:34:42 +01:00
Son
4c33b63f97 better way to know whether this is the last page 2021-11-23 14:44:48 +01:00
Son
cd553608a5 fix 2021-11-23 14:35:37 +01:00
Son
f049da8c9a Compare mx domains with priority order and not priority value 2021-11-23 14:31:53 +01:00
Son
eeb24f594a Add OLD_UNSUBSCRIBER and support it 2021-11-22 18:17:07 +01:00
Son
64d2e7804e improve wording on custom domain DNS page 2021-11-22 17:22:49 +01:00
Son
55ae61527d user can buy 1-year coupon 2021-11-22 16:30:27 +01:00
Son
2d9f8e83e6 add PADDLE_COUPON_ID to config 2021-11-22 16:28:49 +01:00
Son
b5c2d9ee2a fix custom domain not correctly set on /v2/alias/custom/new and /v3/alias/custom/new 2021-11-22 16:20:50 +01:00
Son
3add9e6db8 make sure to delete Fido when disabling Fido on a user 2021-11-22 15:58:11 +01:00
Son
bd8b9526f6 set Fido.user_id 2021-11-22 15:57:51 +01:00
Son
e4f2e1f5a8 add Fido.user_id column 2021-11-22 15:57:38 +01:00
Son
63e1baf46a can disable both OTP and FIDO 2021-11-22 11:32:14 +01:00
Son
dc243d6027 improve logging 2021-11-22 11:23:21 +01:00
Son
04d6ab519b update the custom domain dns issue email template 2021-11-22 10:32:12 +01:00
Son
77e38e63fe handle hotmail complaint during reply phase 2021-11-21 11:31:28 +01:00
Son
bc936436ef explain why deleting a subdomain/directory won't restore the quota 2021-11-20 20:01:36 +01:00
Son
63f4d15329 do not decrease directory or subdomain quota when user deletes a subdomain/directory 2021-11-20 20:00:35 +01:00
Son
a072fdcd96 do not send emails to disabled user 2021-11-20 19:42:49 +01:00
Son
30f2734853 flake8 2021-11-19 18:32:04 +01:00
Son
7c7bf15a13 install flask-debugtoolbar-sqlalchemy to have debug info about sqlalchemy 2021-11-19 18:31:42 +01:00
Son
b2c31ef658 fix the pagination error display 2021-11-19 18:30:36 +01:00
Son
d2ed9337f1 add coupon comment in notification 2021-11-19 16:40:52 +01:00
Son
fdfa286d3e allow contact email to be case sensitive 2021-11-18 16:44:04 +01:00
Son
a17e81a8f1 user can't import csv if current_user.disable_import 2021-11-18 10:51:57 +01:00
Son
6f4c9f6c5a add User.disable_import column 2021-11-18 10:51:13 +01:00
Son
adb376525f flake8 2021-11-18 10:33:38 +01:00
Son
38ecb227b0 reduce subdomain/directory quota when user create/delete subdomain/directory 2021-11-18 10:33:15 +01:00
Son
85c6e791bc add _directory_quota, _subdomain_quota column to User 2021-11-18 10:30:46 +01:00
Son
bccfcee780 add subdomain and directory stats to Metric 2021-11-17 17:43:59 +01:00
Son
ffc04c7fe9 redirect user to subdomain page if a subdomain is deleted 2021-11-17 17:34:53 +01:00
Son
a8c86785d1 make sure a deleted subdomain can't be recreated 2021-11-17 17:21:13 +01:00
Son
5a81c08e32 add DeletedSubdomain model 2021-11-17 17:20:00 +01:00
Son
417f7b92b0 make sure a deleted directory can't be recreated 2021-11-17 17:02:31 +01:00
Son
482aa8614c Create DeletedDirectory model 2021-11-17 17:01:39 +01:00
Son
225a3ae750 handle Yahoo complaint for transactional email 2021-11-17 14:37:49 +01:00
Son
8280acb266 improve email wording 2021-11-17 14:36:47 +01:00
Son
2b8de82028 handle hotmail complain for transactional email 2021-11-17 14:32:30 +01:00
Son
2ce7f3d445 add coupon.comment column 2021-11-17 11:52:46 +01:00
Son
67377a0f22 do not show subdomains on the domain page 2021-11-17 11:52:33 +01:00
Son
6eb702870c handle the case alias is in trash 2021-11-17 10:56:43 +01:00
Son
9c27f94e8e return only bounce report that can be decoded 2021-11-17 10:54:17 +01:00
Son
278a9d19c6 update sentry sdk to 1.4.3 2021-11-17 10:50:02 +01:00
Son
abc074ea9b make sure password can't be longer than 100 chars 2021-11-16 19:41:05 +01:00
Son
6012b6ff54 rename file 2021-11-16 19:39:51 +01:00
Son
96f16b658f add coupon code 2021-11-16 14:43:34 +01:00
Son
d8a23ba9d3 fix email template 2021-11-15 11:55:22 +01:00
Son
8c56fde84d move subdomain to a better place on the menu 2021-11-15 11:52:06 +01:00
Son
eaff8b7ff3 remove User.can_use_subdomain column, make subdomain available to all users 2021-11-15 11:16:03 +01:00
Son
82e0bcec8e add docker build image command for building multi architecture image 2021-11-15 11:13:34 +01:00
Son
33e3227b81 Fix docker build in Mac: install libre2-dev 2021-11-14 19:36:54 +01:00
Son
790f0ed23c return 250 status when handling bounces 2021-11-13 11:21:19 +01:00
Son
324cc8734b handle the case user mistakenly use a lifetime coupon on the coupon page 2021-11-12 17:53:56 +01:00
Son
416eafaeb9 use the first alias suffix when creating a new random alias 2021-11-12 11:04:00 +01:00
Son
bb5259ac3f fix test 2021-11-12 10:00:01 +01:00
Son
611fb8a20c take into account user.include_website_in_one_click_alias in /api/alias/random/new 2021-11-12 09:45:31 +01:00
Son
293cc74c53 add User.include_website_in_one_click_alias column 2021-11-12 09:44:40 +01:00
Son
2fad942c95 include coupon comment in the notification 2021-11-11 18:22:41 +01:00
Son
4fc6619553 reduce Hotmail abuse report rate 2021-11-10 10:57:22 +01:00
Son
4c1c8a3dc1 fix msg[headers.MESSAGE_ID] can return str 2021-11-10 09:38:20 +01:00
Son
e24b84f6bf update error message 2021-11-09 11:21:39 +01:00
Son
5105c0dbee limit the number of subdomains 2021-11-09 10:17:47 +01:00
Son
4c87e4ce68 improve test 2021-11-08 12:57:03 +01:00
Son
e55fae50b8 improve onboarding email wording 2021-11-08 12:34:32 +01:00
Son
72575db8c4 improve mailbox onboarding email wording 2021-11-08 12:29:07 +01:00
Son
9f9b470ab8 fix 2021-11-08 11:52:41 +01:00
Son
d7971953ac return error if name too long when updating alias 2021-11-08 11:26:10 +01:00
Son
89648a83dd fix case /mailbox/confirm_change is called in unauthorized user 2021-11-08 11:22:54 +01:00
Son
0e24513bcf fix case where msg[headers.IN_REPLY_TO] can be non str 2021-11-08 11:21:01 +01:00
Son
827b90432c do not add log for /git and /favicon.ico 2021-11-06 18:26:39 +01:00
Son
429683f444 log more data in apple.verify_receipt() 2021-11-06 18:25:15 +01:00
Son
38e7a64f4f improve daily report 2021-11-06 12:41:37 +01:00
Son
a1fdbc0caa use bigint for Fido.sign_count 2021-11-06 12:40:16 +01:00
Son
bcf1fa2510 fix bug: user can't update mailbox address if it's already used by another account 2021-11-05 18:42:34 +01:00
Son
383f633e41 fix active_page 2021-11-05 18:33:14 +01:00
Son
bfab753e76 do not show subdomain if no SLDomain support it 2021-11-05 18:10:56 +01:00
Son
4ed60ba1d0 set dmarc and dkim to False for subdomain 2021-11-05 18:09:04 +01:00
Son
58e92e7462 user can add subdomain 2021-11-05 11:44:39 +01:00
Son
ef734d7045 add User.can_use_subdomain column 2021-11-05 11:44:04 +01:00
Son
3f1020d5d7 Add CustomDomain.is_sl_subdomain and SLDomain.can_use_subdomain columns 2021-11-05 11:29:10 +01:00
Son
4214efa497 handle the case original_message_id is None in replace_original_message_id 2021-11-05 09:43:58 +01:00
Son
516898af59 move all template files to templates/ 2021-11-04 15:05:22 +01:00
Son
4a47e8c9c6 refactor: move template files to templates/ 2021-11-04 15:00:39 +01:00
Son
0de85fdce3 redirect user directly to the client page if user has already authorized the client 2021-11-04 14:59:01 +01:00
Son
a03d87b62c move files to templates/ 2021-11-04 14:48:56 +01:00
Son
914696ef3b re-enable bounce report in daily report 2021-11-04 14:28:19 +01:00
Son
f8b6b20dd8 not create html in send_email if html isn't set 2021-11-04 14:27:33 +01:00
Son
80bbfb6f4b Parse reverse alias first in handle_hotmail_complaint 2021-11-04 10:40:12 +01:00
Son
5c0cd60659 add mention of 15 aliases limit in the welcome email 2021-11-04 10:27:44 +01:00
Son
fd24f6eb1b improve UI for block/unblock sender 2021-11-03 12:36:20 +01:00
Son
014b7d5b1f improve wording on setting page 2021-11-03 12:36:07 +01:00
Son
0ae40d599a user can block contact directly on the dashboard 2021-11-03 11:29:46 +01:00
Son
2aab48a3f9 show generic error for htmx:responseError event 2021-11-03 11:27:23 +01:00
Son
fa743fc142 install htmx.org 2021-11-03 11:27:02 +01:00
Son
1fd9a344d4 refactor 2021-11-03 10:53:39 +01:00
Son
51a85011b1 rename "send email" to "contacts" 2021-11-03 10:23:40 +01:00
Son
ba16234456 create .jshintrc file, set esversion=8 2021-11-03 10:20:21 +01:00
Son
334dc01a1b fix url 2021-11-03 10:11:52 +01:00
Son
accbf882c4 user can set one_click_unsubscribe_block_sender setting 2021-11-03 10:11:47 +01:00
Son
31e39314d5 return "block_forward" for /api/aliases/{alias.id}/contacts 2021-11-02 15:55:16 +01:00
Son
d81e9fb75f fix manual subscription reminder sent for lifetime user 2021-11-02 15:47:31 +01:00
Son
4369137e25 block the sender via one click unsubscribe 2021-11-02 15:44:43 +01:00
Son
368a2f1b47 Add User.one_click_unsubscribe_block_sender column 2021-11-02 15:41:49 +01:00
Son
caa8656748 create /dashboard/block_contact/:contact_id 2021-11-02 15:30:18 +01:00
Son
fd7d9969f8 create a test for unsubscribe 2021-11-02 15:20:33 +01:00
Son
b50f1d60b2 refactor: create headers constants for List-Unsubscribe 2021-11-02 14:36:37 +01:00
Son
52a19818b7 save email whose bounce info can't be parsed for debugging 2021-11-02 14:32:16 +01:00
Son
4a5983993e rename file 2021-11-02 11:46:41 +01:00
Son
fbb1451352 rename file 2021-11-02 11:43:04 +01:00
Son
d27c19c33a rename file 2021-11-02 11:42:20 +01:00
Son
67a8e0f9cc rename file 2021-11-02 11:41:01 +01:00
Son
48918ba2c1 rename file 2021-11-02 11:40:02 +01:00
Son
cb0d992ecc rename file 2021-11-02 11:37:46 +01:00
Son Nguyen Kim
10be304865
Merge pull request #668 from szepeviktor/patch-2
Remove unused import
2021-11-02 11:02:05 +01:00
Son Nguyen Kim
7524689e8d
Merge pull request #667 from szepeviktor/patch-1
Fix typo
2021-11-02 11:01:22 +01:00
Son
f95428a5cc do not delete email log when email can't be sent to a contact 2021-11-02 10:59:17 +01:00
Son
542310f5ca refactor: rename file 2021-11-02 10:58:51 +01:00
Viktor Szépe
271ddb82f2
Remove unused import 2021-11-02 05:39:06 +01:00
Viktor Szépe
103d550347
Fix another typo 2021-11-02 04:48:13 +01:00
Viktor Szépe
597f8cac74
Fix typo 2021-11-02 04:34:04 +01:00
Son
6b33e66016 handle the case another matching with original_message_id was created in the mean time 2021-11-01 20:39:53 +01:00
Son
5b5bbcc83c refactor: extract replace_sl_message_id_by_original_message_id 2021-11-01 18:45:10 +01:00
Son
2546fefa51 handle message-id replacement for case a reply is sent to multiple recipients 2021-11-01 18:43:19 +01:00
Son
7fef62f67a Add MessageIDMatching.email_log_id column 2021-11-01 18:41:36 +01:00
Son
603e98d0bf remove unnecessary check 2021-11-01 17:58:39 +01:00
Son
db226c5706 black 2021-11-01 17:58:20 +01:00
Son
e67969cdcf add processID to log format 2021-11-01 17:45:24 +01:00
Son
2691fff217 handle UnicodeDecodeError in replace() 2021-11-01 10:11:36 +01:00
Son
e62c5d1591 handle case user has taken a paid subscription in notify_manual_sub_end() 2021-10-31 19:03:49 +01:00
Son
c0aa45fc6d black 2021-10-28 19:07:03 +02:00
Son
74d4aa9f8f comment out bounce report, alias creation report in daily report 2021-10-28 19:01:34 +02:00
Son
ebe727dc53 skip test_can_be_used_as_personal_email 2021-10-28 19:00:41 +02:00
Son
cf8150b996 handle case original_message_id can be None 2021-10-28 18:41:36 +02:00
Son
5f6ad21e85 improve bounce email notification 2021-10-28 11:43:44 +02:00
Son
a4dbbb6ac2 if contact is blocked, do not forward email 2021-10-28 10:19:58 +02:00
Son
507d10cd89 user can block/unblock contact 2021-10-28 10:19:34 +02:00
Son
bc4805b1fa Add POST /api/contacts/:contact_id/toggle 2021-10-28 10:14:20 +02:00
Son
9620f97449 add Contact.block_forward column 2021-10-28 10:12:56 +02:00
Son
02a005d076 increase message_id length to 1024 2021-10-27 16:06:56 +02:00
Son
9d3711a98a improve cron report 2021-10-27 11:48:42 +02:00
Son
35256bcdeb install git in dockerfile 2021-10-27 11:20:18 +02:00
Son
78ad9fbcc5 improve daily report 2021-10-26 13:03:57 +02:00
Son
286717dae3 use plaintext for report 2021-10-26 12:48:01 +02:00
Son
7c182d20a4 try parsing the alias from the from header which might contain the reverse alias when handling hotmail complaint 2021-10-26 12:16:57 +02:00
Son
8aa7b1b773 set client.referral_id=None when a referral is deleted 2021-10-26 12:06:49 +02:00
Son
b41b695228 include the referral code when user signs up via SIWSL 2021-10-26 12:06:16 +02:00
Son
04bcc24ad7 user can set client.referral 2021-10-26 12:04:16 +02:00
Son
1aff59e112 improve client UI 2021-10-26 11:55:42 +02:00
Son
f19655fc93 add client.referral_id column 2021-10-26 11:55:27 +02:00
Son
a99ac24b72 cron, init app, job runner: wrap in an app context to benefit from app setup like database cleanup, sentry integration, etc 2021-10-26 10:52:28 +02:00
Son
a0165d6381 remove not working on gmail part 2021-10-25 15:39:49 +02:00
Son
3d071d27a6 improve wording, add client url 2021-10-25 15:18:42 +02:00
Son
0fbd351bed handle the referral url that has ?slref=code part 2021-10-25 15:02:02 +02:00
Son
83c5eded80 Referral name is required 2021-10-25 15:01:32 +02:00
Son
8cb413d5fd remove unused import 2021-10-25 14:47:07 +02:00
Son
7e0609c39a do not display nb_paid_user for client 2021-10-25 14:36:23 +02:00
Son
d701b84110 decode and encode email payload for quoted-printable email in replace() 2021-10-25 14:34:13 +02:00
Son
8680c0a739 do do not use the ra+ prefix for reverse alias 2021-10-25 14:33:42 +02:00
Son
befec56a86 display nb paid user on SIWSL app 2021-10-25 11:10:23 +02:00
Son
8bafdfc879 improve SIWSL wording 2021-10-25 11:09:30 +02:00
Son
de0f838950 ignore hotmail bounce that uses 'Undisclosed recipients:;' in :To header 2021-10-25 10:36:50 +02:00
Son
9299904fc9 small refactor 2021-10-24 10:40:05 +02:00
Son
6468f7c8a5 improve bounce report 2021-10-24 10:39:47 +02:00
Son
ef5670b1cf add all bounce to daily report 2021-10-23 18:40:02 +02:00
Son
083b56b9a6 if user.disable_automatic_alias_note, do not add alias note 2021-10-23 18:24:28 +02:00
Son
50b0dc3767 Add User.disable_automatic_alias_note column 2021-10-23 17:40:57 +02:00
Son
d41ab5f5de use words.txt instead of words_alpha.txt 2021-10-23 17:31:56 +02:00
Son
ad24f19cd6 rename words.txt to test_words.txt 2021-10-23 17:29:37 +02:00
Son
b0822519eb only show the navigation arrow when #contacts > PAGE_LIMIT 2021-10-23 16:08:43 +02:00
Son
ff210394a0 rename canonical_url -> CANONICAL_URL 2021-10-23 16:07:54 +02:00
Son
4a90c79753 make sure mailbox_ids is a list in /api/v3/alias/custom/new 2021-10-23 15:55:39 +02:00
Son
ecdce2307f make sure input of /api/v3/alias/custom/new is a dict 2021-10-23 15:52:17 +02:00
Son
e411e09779 add edge add-on to footer 2021-10-22 15:32:01 +02:00
Son
5843fa94a0 handle encoding typo 2021-10-19 18:05:56 +02:00
Son
9fb6e45077 fix the help text too close to the input 2021-10-19 17:38:29 +02:00
Son
421c121d59 black 2021-10-19 14:03:51 +02:00
Son
be7ae3021a rename is_reply_email -> is_reverse_alias, make sure reverse-alias must end with EMAIL_DOMAIN 2021-10-19 12:14:16 +02:00
Son
c5987bcfbb Log message_id 2021-10-19 12:05:41 +02:00
Son
a2fcfbbb20 use as_bytes().decode() instead of as_string() in bounce_info 2021-10-19 12:05:35 +02:00
Son
7952ce7ecf sanity check to make sure the message id hasn't been added before 2021-10-19 11:58:05 +02:00
Son
c12f3b3e7a Use SL message ID during reply phase. Exchange original SL and original message-id during the forward and reply phase to keep email thread. 2021-10-18 17:35:16 +02:00
Son
9c653dbacd no need to fill up message-id header: it seems always filled 2021-10-18 17:34:11 +02:00
Son
1483f2e103 Add EmailLog.message_id, sl_message_id and MessageIDMatching table 2021-10-18 17:25:59 +02:00
Son
462164ff16 use tldextract to extract hostname 2021-10-18 11:45:48 +02:00
Son
af221998f3 install tldextract 2021-10-18 11:45:08 +02:00
Son
7d33f10c05 install gpg 2021-10-18 11:25:10 +02:00
Son
d6edd59450 use warning level 2021-10-18 09:55:28 +02:00
Son Nguyen Kim
9f36f3e2a9
Merge pull request #654 from LordChunk/master
Improved Docker image size
2021-10-18 09:45:34 +02:00
Son
0b06c46f65 handle missing content-transfer-encoding 2021-10-17 17:19:44 +02:00
Son
283a6a530d handle case address.parse can also parse an URL and return UrlAddress 2021-10-17 12:52:59 +02:00
Job
742bfd6815
Merge branch 'simple-login:master' into master 2021-10-15 14:09:52 +02:00
Job
4e5ca3b30b Added apt folder cleaning 2021-10-15 11:20:07 +00:00
Job
be82600fe6 Fixed docker builds 2021-10-15 10:59:33 +00:00
Son
7bfdb821af compute and include nb_total_bounced_last_24h in email report 2021-10-15 10:47:07 +02:00
Son
34564f6fa4 Add Metric2.nb_total_bounced_last_24h column 2021-10-15 10:46:22 +02:00
Son
4d740a4dc0 flake8 2021-10-15 10:39:29 +02:00
Son
d18bb28ca9 add more log to investigate "Cannot parse Postfix queue ID" error 2021-10-15 10:37:22 +02:00
Son
57bfa7e933 make sure that a domain already used in a verified mailbox can't be added 2021-10-15 10:32:20 +02:00
Son
72931aa9b7 fill up Bounce.info 2021-10-14 15:46:52 +02:00
Son
fcb94f0331 add Bounce.info column 2021-10-14 15:45:29 +02:00
Son
7add04accc Use alembic instead of flask migrate which depends on flask-sqlalchemy 2021-10-14 15:45:17 +02:00
Son
3bdeda3e04 add get_mailbox_bounce_info() 2021-10-14 15:10:16 +02:00
Son
e5a7aeb3fb add sl-job-runner to self host instruction 2021-10-13 19:16:20 +02:00
Son
05cf085511 fix 2021-10-13 13:32:27 +02:00
Son
ced31edda2 flake8 2021-10-13 11:52:41 +02:00
Son
cfe88b5df2 use job system to delete domain 2021-10-13 11:43:44 +02:00
Son
fbabe6fb44 use job system for deleting mailbox 2021-10-13 11:40:15 +02:00
Son
3a0b125323 fix table name 2021-10-13 10:52:41 +02:00
Son
e13a974e53 disable rate limiting 2021-10-13 10:30:04 +02:00
Son
68cf54b2d9 Revert "use async in email handler"
This reverts commit 4d7cd09847.
2021-10-13 10:27:59 +02:00
Son
0ec4a3971c Revert "sleep for 60s when rate limit is hit for the first time"
This reverts commit 2524c8ab98.
2021-10-13 10:27:54 +02:00
Son
becf789d5e Revert "add more logging"
This reverts commit 94a9a1479b.
2021-10-13 10:27:46 +02:00
Son
94a9a1479b add more logging 2021-10-13 10:18:46 +02:00
Job
4451e6af33
Merge pull request #1 from LordChunk/reduce-docker-image-size
Improved Docker image size
2021-10-13 01:48:31 +02:00
Job
961daa91f3
Improved Docker image size
Improved Docker image size by using python's alpine image and installing the required dependencies seperately. 
This reduces the size of the image from 1.46 GB to 0.982 GB
2021-10-13 01:45:48 +02:00
Son
572f25ff75 change header item position 2021-10-12 15:15:37 +02:00
Son
b9d26d46f6 fix test 2021-10-12 15:11:25 +02:00
Son
c132e3fbbc flake8 2021-10-12 15:03:16 +02:00
Son
2524c8ab98 sleep for 60s when rate limit is hit for the first time 2021-10-12 14:53:30 +02:00
Son
a8b3955fe6 black 2021-10-12 14:52:57 +02:00
Son
4d7cd09847 use async in email handler 2021-10-12 14:48:33 +02:00
Son
eb0e327402 remove "with app.app_context():" 2021-10-12 14:47:01 +02:00
Son
074dd875dc comment out "Submit for approval" section 2021-10-12 14:39:29 +02:00
Son
372466ab06 do not use flask-sqlalchemy
- add __tablename__ for all models
- use sa and orm instead of db
- rollback all changes in tests
- remove session in @app.teardown_appcontext
2021-10-12 14:36:47 +02:00
Son
653a03ac11 show tooltip on highlighted alias 2021-10-12 09:58:17 +02:00
Son
8394d7340c format 2021-10-12 09:51:11 +02:00
Son
b602f7e746 update send from alias video 2021-10-11 17:08:48 +02:00
Son
ee0ed7d9ec refactor: use headers.py 2021-10-11 12:21:14 +02:00
Son
d6fc132df1 do not replace message-id in reply phase 2021-10-11 12:13:24 +02:00
Son
5821294ae9 refactor: use headers.py 2021-10-11 12:10:18 +02:00
Son
9bb83fe3e2 fix email thread: do not delete original message id 2021-10-11 12:00:37 +02:00
Son
a7f82b2110 fix test 2021-10-11 11:47:07 +02:00
Son
5d7e10f776 make sure when user changes password, log user out on other browsers 2021-10-11 11:30:41 +02:00
Son
fdc23b3107 add User.alternative_id column 2021-10-11 11:30:10 +02:00
Son
f525c951c6 update footer links 2021-10-11 11:12:40 +02:00
Son
ea3ac5697b add link to docs on header 2021-10-06 10:29:40 +02:00
Son
cef6579946 add User.lifetime_coupon_id column 2021-10-04 17:14:34 +02:00
Son
c7626dd23e add how to install pyre2 in Contributing 2021-10-04 16:48:44 +02:00
Son
9c528b913c add LifetimeCoupon.comment column 2021-10-04 16:48:33 +02:00
Son
ba6ed3cba7 black 2021-10-03 19:44:39 +02:00
Son
d622d95c35 fix test 2 2021-10-03 19:36:30 +02:00
Son
931e924f8c fix test 2021-10-03 19:34:51 +02:00
Son
4638155bbc allow import aliases for domains that have ownership_verified 2021-10-02 19:18:38 +02:00
Son Nguyen Kim
9acfda0fba
Merge pull request #628 from prashantkamdar/master
Binding the docker containers to localhost
2021-10-01 19:01:28 +02:00
prashantkamdar
fbf1ca3395 hiding the postgres port during upgrade 2021-10-01 21:38:07 +05:30
prashantkamdar
202fadcfc8 removing extra space 2021-09-30 22:37:44 +05:30
prashantkamdar
8356a9627d updating the readme and upgrade docs to bind to localhost 2021-09-30 22:36:30 +05:30
Prashant Kamdar
71b7c18ae8
Merge branch 'simple-login:master' into master 2021-09-30 22:30:17 +05:30
Son Nguyen Kim
9528bdcb2e user can enable the ignore_loop_email 2021-09-27 15:58:04 +02:00
Son Nguyen Kim
e3f81bc4e4 fix subscription reminder sent to lifetime user 2021-09-27 15:51:09 +02:00
Son Nguyen Kim
339d611e63 remove Contact.from_header column 2021-09-27 12:19:33 +02:00
Son Nguyen Kim
8301015afd do not use email_validator in get_email_domain_part() 2021-09-27 12:13:41 +02:00
Son Nguyen Kim
3ad961bfb9 ignore contact name that has hex ascii code \x00 2021-09-27 10:21:49 +02:00
Son Nguyen Kim
408322217d allow user having apple subscription to switch to web subscription 2021-09-27 09:59:33 +02:00
Son Nguyen Kim
51a7dbfa52 Revert "not dkim sign"
This reverts commit b14534db2c.
2021-09-25 18:47:15 +02:00
Son Nguyen Kim
b14534db2c not dkim sign 2021-09-23 19:32:06 +02:00
Son Nguyen Kim
469c2011aa use ~all instead of -all for SPF 2021-09-23 10:30:59 +02:00
Son Nguyen Kim
486dd831cf fix canonical url 2021-09-23 09:25:07 +02:00
Son Nguyen Kim
0ed0ac9ea7 handle "text/x-python-script" in replace() 2021-09-23 09:19:07 +02:00
Son Nguyen Kim
7f5201effa handle ValueError raised by parse_full_address 2021-09-22 16:39:31 +02:00
Son Nguyen Kim
c5425b0a73 black 2021-09-22 16:04:57 +02:00
Son Nguyen Kim
d7d301b9c3 add missing h1 2021-09-22 16:03:58 +02:00
Son Nguyen Kim
2e6b012eff set canonical tag for all html pages 2021-09-22 15:33:08 +02:00
Son Nguyen Kim
3b16e502b3 add debug info when an email is sent from reverse-alias 2021-09-22 09:58:40 +02:00
Son Nguyen Kim
3443499ab9 add note for alias auto created with directory too 2021-09-22 09:45:42 +02:00
Son Nguyen Kim
1a32b654d0 refactor 2021-09-22 09:45:00 +02:00
Son Nguyen Kim
7674d8480e refactor: rename 2021-09-22 09:44:35 +02:00
Son Nguyen Kim
ff1238a56f add alias note when auto creating alias via domain 2021-09-22 09:43:48 +02:00
Son Nguyen Kim
16dd35470f add more debug info 2021-09-21 14:20:46 +02:00
Son Nguyen Kim
8024b35f1d refactor 2021-09-21 14:11:59 +02:00
Son Nguyen Kim
d8280af93c refactor: rename 2021-09-21 14:09:24 +02:00
Son Nguyen Kim
5e9fb83150 fix typo 2021-09-21 14:08:52 +02:00
Son Nguyen Kim
636879ac1a use another icon for pin 2021-09-21 12:20:44 +02:00
Son Nguyen Kim
9279b20975 only put pinned alias in default sorting 2021-09-21 12:20:29 +02:00
Son Nguyen Kim
0075cee1ee default to UUID for api key code if the previous one is already used 2021-09-21 11:27:37 +02:00
Son Nguyen Kim
3c81f982ca display N/A if api key name is null 2021-09-21 11:26:05 +02:00
Son Nguyen Kim
65ce47b6f7 Allow ApiKey.name to be null 2021-09-21 11:25:52 +02:00
Son Nguyen Kim
18acfd9a42 handle case mailbox new address is already used 2021-09-21 11:19:22 +02:00
Son Nguyen Kim
19088ba85f refactor 2021-09-21 11:17:32 +02:00
Son Nguyen Kim
d9d67df126 remove unique constraint on TransactionalEmail.email 2021-09-21 11:15:40 +02:00
Son Nguyen Kim
8d40392b5c allow bare "accept" in flake8 2021-09-21 11:08:27 +02:00
Son Nguyen Kim
bcc5126500 remove any restriction on regex 2021-09-21 10:57:47 +02:00
Son Nguyen Kim
1d09d76cb2 use re2 instead of re to avoid ReDOS attack 2021-09-21 10:57:36 +02:00
Son Nguyen Kim
1a6c68e98d install pyre2 2021-09-21 10:54:35 +02:00
Son Nguyen Kim
2c60414796 when new rule is created, go to the rule list section 2021-09-21 10:43:02 +02:00
Son Nguyen Kim
84880ae32a fix error with rule regex doesn't save if error 2021-09-21 10:42:32 +02:00
Son Nguyen Kim
1e3afa257c validate regex before creating rule 2021-09-21 10:42:02 +02:00
Son Nguyen Kim
f160ebec4e add debug zone to auto create page 2021-09-21 10:14:36 +02:00
Son Nguyen Kim
809f547742 remove CustomDomain.auto_create_regex 2021-09-21 09:48:07 +02:00
Son Nguyen Kim
e5a8ce1492 use AutoCreateRule instead of custom_domain.auto_create_regex when creating new alias 2021-09-20 18:29:36 +02:00
Son Nguyen Kim
56c72d5fba create auto create page, remove custom domain auto_create_regex part 2021-09-20 18:28:43 +02:00
Son Nguyen Kim
f36f8b94e2 Create AutoCreateRule, AutoCreateRuleMailbox model 2021-09-20 18:23:19 +02:00
Son Nguyen Kim
0055ca976b add flush option to ModelMixin.create() 2021-09-20 18:16:52 +02:00
Son Nguyen Kim
ba3074b94a use warning instead of error 2021-09-20 16:59:27 +02:00
Son Nguyen Kim
f6fd97ef05 log total number of email log for an alias when it is rate limited 2021-09-20 13:54:29 +02:00
Son Nguyen Kim
17c13ee37f ignore smtp error in handle_bounce_forward_phase() 2021-09-20 13:51:16 +02:00
Son Nguyen Kim
cfb7b7cefc fix setting 2021-09-20 13:43:54 +02:00
Son Nguyen Kim
005a760710 handle case catch_all is enabled but custom_domain.auto_create_regex is already set 2021-09-20 12:32:39 +02:00
Son Nguyen Kim
0aa3dff38b handle case pg_trgm can't be dropped when running test 2021-09-20 12:28:12 +02:00
Son Nguyen Kim
153831ed1a remove obsolete sender formats 2021-09-20 12:27:36 +02:00
Son Nguyen Kim
7bb54e1e8e add "Custom/number_incoming_email" in newrelic 2021-09-20 09:48:06 +02:00
Son Nguyen Kim
98b472d925 use "pin" instead of "favorite" 2021-09-19 19:50:50 +02:00
Son Nguyen Kim
530bc8591e support | in auto create alias regex 2021-09-19 09:36:19 +02:00
Son Nguyen Kim
cbc20dd268 remove unused import 2021-09-18 19:19:17 +02:00
Son Nguyen Kim
838f1dc86d add beta mention to auto create alias regex 2021-09-18 19:16:43 +02:00
Son
9b89d7cc5d update package 2021-09-18 18:48:31 +02:00
Son
ec2812bfa4 handle invalid email in email_can_be_used_as_mailbox 2021-09-18 18:46:26 +02:00
Son Nguyen Kim
8b676bc4af not disable alias if ALIAS_AUTOMATIC_DISABLE is not set 2021-09-17 18:05:35 +02:00
Son Nguyen Kim
de3207ac4b Add ALIAS_AUTOMATIC_DISABLE 2021-09-17 18:05:18 +02:00
Son Nguyen Kim
344f8e67d2 take into account custom_domain.auto_create_regex in try_auto_create_catch_all_domain() 2021-09-17 17:43:12 +02:00
Son Nguyen Kim
a6c874e914 Use validate_email in get_email_local_part and get_email_domain_part 2021-09-17 17:42:52 +02:00
Son Nguyen Kim
2b84168d68 validate address in try_auto_create 2021-09-17 17:42:16 +02:00
Son Nguyen Kim
0b127216ee user can set custom_domain.auto_create_regex 2021-09-17 17:41:36 +02:00
Son Nguyen Kim
58d36e9cd8 add CustomDomain.auto_create_regex column 2021-09-17 17:35:36 +02:00
Son Nguyen Kim
b990c052ac move domain mailboxes to domain detail page and only enable it if catch-all is on 2021-09-17 11:54:37 +02:00
Son Nguyen Kim
512ade83b4 improve wording & styling on custom domain info page 2021-09-17 11:38:07 +02:00
Son Nguyen Kim
785a619385 Add debug_info decorator 2021-09-17 10:31:26 +02:00
Son Nguyen Kim
68d33ea85b upgrade flask-sqlalchemy to 2.5.1 2021-09-17 10:30:26 +02:00
Son Nguyen Kim
981f6ecfb2 handle the case pg_trgm is already loaded 2021-09-16 18:02:45 +02:00
Son Nguyen Kim
da0ddd5a34
Merge pull request #619 from Dattito/fix-wrong-description-of-error
fixed wrong description of error
2021-09-15 11:23:11 +02:00
Son Nguyen Kim
695a628e68 save email that can't be DKIM signed to temp dir to investigate 2021-09-15 09:29:08 +02:00
Son Nguyen Kim
144418ae47 use debug level when postfix queue id can't be parsed 2021-09-15 09:28:27 +02:00
Son Nguyen Kim
3441d2ccf1 add new param TEMP_DIR 2021-09-15 09:28:08 +02:00
David Siregar
1cc8f7f2e3 fixed wrong description of error 2021-09-14 21:28:51 +02:00
Son Nguyen Kim
567bee9a0b add exception trace when dkim fails 2021-09-14 09:13:39 +02:00
Son Nguyen Kim
8990895dd2 improve email wording 2021-09-14 09:03:25 +02:00
Son Nguyen Kim
e4ed192cce rename 2021-09-13 19:50:15 +02:00
Son Nguyen Kim
106358da5f handle yahoo complaint 2021-09-13 19:49:40 +02:00
Son Nguyen Kim
b3012376c3 use another DKIM header if one fails 2021-09-13 16:04:32 +02:00
Son Nguyen Kim
6e42e536db ignore email sent from a mailbox to its alias if user.ignore_loop_email 2021-09-10 18:15:22 +02:00
Son Nguyen Kim
62044e6db1 Add User.ignore_loop_email column 2021-09-10 18:14:51 +02:00
Son Nguyen Kim
f53e8c1af8 refactor 2021-09-10 17:48:36 +02:00
Son Nguyen Kim
4949afc791 black 2021-09-10 17:42:07 +02:00
Son Nguyen Kim
e5a388dffb optimize import 2021-09-10 17:37:33 +02:00
Son Nguyen Kim
61d9f7ee43 refactor 2021-09-10 17:31:29 +02:00
Son Nguyen Kim
41478a5715 replace parseaddr_unicode by parse_full_address 2021-09-10 17:26:14 +02:00
Son Nguyen Kim
638e8137ec fix test 2021-09-10 17:10:05 +02:00
Son Nguyen Kim
3ad4b6b76f use flanker instead of parseaddr_unicode 2021-09-10 17:06:38 +02:00
Son Nguyen Kim
500ff00c7c use flanker to parse To:, CC: header in replace_header_when_forward() 2021-09-10 16:51:36 +02:00
Son Nguyen Kim
8023afe9be use email_validator instead of validate_email which isn't updated for a while 2021-09-10 16:42:02 +02:00
Son Nguyen Kim
6b65e00dcf install flanker, upgrade email_validator 2021-09-10 16:36:59 +02:00
Son Nguyen Kim
6e9dfdd6f1 if the complaint cannot be handled, forward it normally 2021-09-09 18:55:29 +02:00
Son Nguyen Kim
db55f289c1 upgrade aiosmtpd to 1.4.2 2021-09-09 11:47:14 +02:00
Son Nguyen Kim
defd7b159d Fix get_header_unicode: handle the case header contains several parts 2021-09-09 11:47:01 +02:00
Son Nguyen Kim
493a5daa45 use warning level 2021-09-08 18:25:40 +02:00
Son Nguyen Kim
ff2cbeb3af handle case to header isn't present 2021-09-08 15:49:47 +02:00
Son Nguyen Kim
a58cf9dd5e use warning level for log 2021-09-08 15:23:48 +02:00
Son Nguyen Kim
4df83f953d handle utf-8 decoding fail 2021-09-08 15:17:11 +02:00
Son Nguyen Kim
5ac78f2694 reformat 2021-09-08 11:29:55 +02:00
Son Nguyen Kim
a6e8684afb add more debug info 2021-09-08 11:25:53 +02:00
Son Nguyen Kim
201eb3b9b9 fix handle_hotmail_complaint: handle email format with name 2021-09-08 11:02:35 +02:00
Son Nguyen Kim
41f10373d1 add plan info 2021-09-08 10:51:47 +02:00
Son Nguyen Kim
f3cff1f1bf create manual subscription using the coupon.is_giveaway info 2021-09-07 15:36:19 +02:00
Son Nguyen Kim
d9f44437da add Coupon.is_giveaway column 2021-09-07 15:35:55 +02:00
Son Nguyen Kim
48838eb176 add highlighted alias in case it's not included in the result 2021-09-07 15:22:50 +02:00
Son Nguyen Kim
b2ac1b537d add only pinned alias filter 2021-09-07 15:10:37 +02:00
Son Nguyen Kim
6dd6b74073 black 2021-09-07 11:13:28 +02:00
Son Nguyen Kim
b53da25a41 handle hotmail complaint 2021-09-06 19:44:18 +02:00
Son Nguyen Kim
ccb526faa1 blur out other aliases when an alias is highlighted 2021-09-06 19:12:31 +02:00
Son Nguyen Kim
1df5bec8df use parsleyjs instead of formbouncerjs 2021-09-06 18:51:50 +02:00
Son Nguyen Kim
ffd2ec5e81 add filter by directory on dashboard 2021-09-05 19:11:05 +02:00
Son Nguyen Kim
3faf5c921d add admin for custom domain 2021-08-24 19:43:32 +02:00
Son Nguyen Kim
8b86851530
Use AGPL license instead of MIT 2021-08-21 19:03:48 +02:00
Son Nguyen Kim
ef6388887f better filter app 2021-08-21 17:56:23 +02:00
Son Nguyen Kim
d6e48ea2e4
Merge pull request #593 from boarwell/keyboard-focusable
Re: Make the "Create" button focusable with keyboard
2021-08-21 16:25:20 +02:00
Son Nguyen Kim
40915ad741 make alias description font smaller 2021-08-21 16:18:01 +02:00
Son Nguyen Kim
4b184998bc make alias note always visible. Rename it to description 2021-08-21 16:11:17 +02:00
Son Nguyen Kim
b5c827c2ea handle case user already has an active subscription via another channel (Paddle, Apple, etc) on coupon page 2021-08-21 16:04:32 +02:00
Son Nguyen Kim
513f5cd4fb rename 2021-08-21 15:55:22 +02:00
boarwell
8519d06639 make the "Create" button focusable with keyboard
fix https://github.com/simple-login/app/pull/561#issuecomment-899102494
2021-08-21 01:09:40 +09:00
Son Nguyen Kim
bae9a6f431 flake8 2021-08-20 16:09:22 +02:00
Son Nguyen Kim
76c1b3d807 use deepcopy instead of email.message_from_string in copy() 2021-08-20 16:03:22 +02:00
Son Nguyen Kim
51578ce934 add filter by mailbox 2021-08-20 12:21:27 +02:00
Son Nguyen Kim
00b3d716b7 load pg_trgm when running test 2021-08-20 12:20:38 +02:00
Son Nguyen Kim
b606d35c11 add pg_trgm index on Alias.note to speed up LIKE search 2021-08-20 12:14:20 +02:00
Son Nguyen Kim
0a1f545c12 improve script 2021-08-20 12:00:45 +02:00
Son
009e1edced also support substring search 2021-08-19 18:07:11 +02:00
Son
d8cb327b6e fix search 2021-08-19 17:47:10 +02:00
Son Nguyen Kim
59e4dbb6a6 fix error display on domain ownership check 2021-08-17 19:39:58 +02:00
Son Nguyen Kim
9c6f3989a0 remove hack 2021-08-17 19:19:49 +02:00
Son Nguyen Kim
aa041708e3 add ownership verification via TXT record 2021-08-17 19:05:12 +02:00
Son Nguyen Kim
f4fead2542 generate a domain ownership txt token if needed 2021-08-17 19:03:15 +02:00
Son Nguyen Kim
52e2e67081 add CustomDomain ownership_verified, ownership_txt_token column. Set ownership_verified=True for domain that has verified=True 2021-08-17 19:02:35 +02:00
Son
e03f9d2342 black 2021-08-15 22:17:49 +02:00
Son
cc86f698ee add login_as to admin 2021-08-15 22:14:52 +02:00
Son
be418029bd fix github ci 2021-08-15 21:49:56 +02:00
Son
800e866663 fix the alias creation not working 2021-08-15 18:18:23 +02:00
prashantkamdar
1e2d682351 typo fix 2021-08-15 21:35:54 +05:30
Son
1678945d5a improve wording 2021-08-15 17:58:49 +02:00
prashantkamdar
173b509706 security steps in the readme 2021-08-15 21:26:38 +05:30
Son
0f4ad1a0d4 black 2021-08-15 17:56:31 +02:00
Son
e5308932a2 make mailbox deletion async 2021-08-15 17:50:47 +02:00
prashantkamdar
e22af08e0b security steps in the readme 2021-08-15 21:19:55 +05:30
prashantkamdar
bf39b924dd security steps in the readme 2021-08-15 21:17:54 +05:30
prashantkamdar
5bf8b75a11 security steps in the readme 2021-08-15 21:15:23 +05:30
Son
66bafe7439 flake8 2021-08-15 17:42:15 +02:00
Son
d9c682a23e remove sqlite everywhere, only use postgres. Do not use 5432 port to avoid conflict 2021-08-15 17:41:16 +02:00
Son
4cbbf260d4 add dummy-data flask command 2021-08-15 17:32:54 +02:00
Son
1384ccc459 remove RESET_DB config 2021-08-15 17:32:33 +02:00
Son Nguyen Kim
888de34a69
Merge pull request #561 from boarwell/keyboard-focusable
Make the "Create" button focusable with keyboard
2021-08-14 22:08:42 +02:00
boarwell
e0da867b4a make the "Create" button focusable with keyboard 2021-08-09 16:49:03 +09:00
Son Nguyen Kim
2e9b288d7b optimize get_alias_infos_with_pagination_v3 when searching on mailbox email 2021-08-06 09:18:14 +02:00
Son Nguyen Kim
12150a3656 Update CONTRIBUTING to add postgres step 2021-08-06 09:14:55 +02:00
Son Nguyen Kim
a13953e13f add postgres to github action 2021-08-06 08:54:24 +02:00
Son Nguyen Kim
142dcafb99 set pyenv version 2021-08-06 08:50:15 +02:00
Son Nguyen Kim
07c912fd35 use postgres database in test instead of sqlite 2021-08-06 08:50:10 +02:00
Son Nguyen Kim
006a7b1420 black 2021-08-06 08:46:38 +02:00
Son Nguyen Kim
348c2271c6 fix test 2021-08-06 08:46:34 +02:00
Son Nguyen Kim
264bab965a fix test 2021-08-05 19:49:36 +02:00
Son Nguyen Kim
012c6fc3fb replace get(1) by first() 2021-08-05 19:44:56 +02:00
Son Nguyen Kim
2f8f354f28 fix error with match(): use plainto_tsquery instead 2021-08-05 19:44:13 +02:00
Son Nguyen Kim
91d3d11452 update wording 2021-08-05 17:37:35 +02:00
Son
51995954f0 fix migration 2021-08-04 16:59:21 +02:00
Son
8d6ff446d8 use raw sql to create alias.ts_vector column 2021-08-04 16:57:31 +02:00
Son Nguyen Kim
8640f830f2 try fixing migration 2021-08-04 09:33:02 +02:00
Son Nguyen Kim
9eb3c7cf2c use Alias.ts_vector instead of note when returning alias 2021-08-04 09:30:12 +02:00
Son Nguyen Kim
2b048543d3 add Alias.ts_vector column to use full text search 2021-08-04 09:29:56 +02:00
Son Nguyen Kim
e2fea3aed8 Revert "add index for Alias name and email column"
This reverts commit 35a9a723aa.
2021-08-04 09:01:59 +02:00
Son Nguyen Kim
35a9a723aa add index for Alias name and email column 2021-08-04 08:57:13 +02:00
Son Nguyen Kim
b9d1d10473 Revert "add some indexes to speed up search by query"
This reverts commit e9538a62be.
2021-08-04 08:56:11 +02:00
Son Nguyen Kim
e9538a62be add some indexes to speed up search by query 2021-08-03 19:13:48 +02:00
Son Nguyen Kim
fb29503b81 do not send bounce to IgnoreBounceSender 2021-08-02 11:33:58 +02:00
Son Nguyen Kim
6dac717c75 Add IgnoreBounceSender model 2021-08-02 11:30:29 +02:00
Son Nguyen Kim
383cd49f25 fix year copyright in email 2021-07-30 17:05:16 +02:00
Son Nguyen Kim
9968cbfa8e make sure user cancels the Paddle subscription before deleting their account 2021-07-29 12:09:40 +02:00
Son Nguyen Kim
fcb18e66e8 fix increase_percent 2021-07-29 11:32:15 +02:00
Son Nguyen Kim
cbcae31288 add troubleshooting doc 2021-07-29 10:43:36 +02:00
Son Nguyen Kim
1cca7d4025 prettify UI 2021-07-29 10:14:13 +02:00
Son Nguyen Kim
6c12b31060 make sure to not notify alias in HibpNotifiedAlias 2021-07-29 09:55:36 +02:00
Son Nguyen Kim
5821bd6512 Create HibpNotifiedAlias to store all notified aliases 2021-07-29 09:41:46 +02:00
Son Nguyen Kim
fb4cb8727c Add notify_hibp cron job 2021-07-29 09:35:00 +02:00
Son Nguyen Kim
5aef6cceb2 Add description, date column to Hibp model 2021-07-29 08:51:21 +02:00
Son Nguyen Kim
dc83c3dd9e Add filter for Only Aliases Found In Data Breaches 2021-07-29 08:45:52 +02:00
Son Nguyen Kim
77c993b864 remove unused get_alias_infos_with_pagination_v2 2021-07-28 18:48:10 +02:00
Son Nguyen Kim
91fdf1ade0
Merge pull request #552 from simple-login/snyk-upgrade-b62aec63143e53a0dfc449f731cf80ab
[Snyk] Upgrade vue from 2.6.13 to 2.6.14
2021-07-28 18:43:58 +02:00
Son Nguyen Kim
52376484a5 Add nb_block_last_24h, nb_bounced_last_24h, nb_forward_last_24h, nb_reply_last_24h 2021-07-28 18:31:59 +02:00
Son Nguyen Kim
39e2750486 remove Metric 2021-07-28 18:20:18 +02:00
Son Nguyen Kim
67cd7ae3d4 keep Reply-To header, replace it by a reverse-alias 2021-07-28 09:12:52 +02:00
Son Nguyen Kim
8e72d79837 add coupon page on the pricing page 2021-07-25 10:58:41 +02:00
Son Nguyen Kim
518c102642 add newrelic to poetry 2021-07-23 15:53:39 +02:00
Son Nguyen Kim
d706bbbd4b Log "Custom/email_handler_time" to NewRelic 2021-07-23 15:48:50 +02:00
Son Nguyen Kim
8ab840933f Add NEWRELIC_CONFIG_PATH config 2021-07-23 15:48:50 +02:00
snyk-bot
29c5b12680
fix: upgrade vue from 2.6.13 to 2.6.14
Snyk has created this PR to upgrade vue from 2.6.13 to 2.6.14.

See this package in npm:
https://www.npmjs.com/package/vue

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2021-07-23 02:30:04 +00:00
Son Nguyen Kim
4f4d487e28
Merge pull request #515 from simple-login/snyk-upgrade-f6ee324f1625f8c0ffd6f70e7f68923f
[Snyk] Upgrade vue from 2.6.12 to 2.6.13
2021-07-22 10:42:40 +02:00
Son Nguyen Kim
52fb01ed8d take into account expand_alias_info on the dashboard 2021-07-22 10:30:38 +02:00
Son Nguyen Kim
c0fe72ccd0 user can change the expand_alias setting 2021-07-22 10:30:17 +02:00
Son Nguyen Kim
8de9931b28 Add User.expand_alias_info column 2021-07-22 10:17:23 +02:00
Son Nguyen Kim
a58aaf8399 add disable OTP to admin page 2021-07-22 09:09:13 +02:00
Son Nguyen Kim
f9b71a4bf4 add journalist upgrade into admin page 2021-07-21 15:08:25 +02:00
Son Nguyen Kim
d181cd49dd Sign the whole Alias Suffix Info instead of just the suffix 2021-07-19 20:14:59 +02:00
Son Nguyen Kim
3141bf1367 remove redundant check 2021-07-19 20:02:26 +02:00
Son Nguyen Kim
06c2114534 add the "in the last 14 days" mention 2021-07-19 18:43:41 +02:00
Son Nguyen Kim
55dcd63654 support search on contact page 2021-07-14 18:57:25 +02:00
Son Nguyen Kim
ac1f56f206 add in the last 14 days mention on the contact page 2021-07-14 17:23:33 +02:00
Son Nguyen Kim
aa799fa339 Revert "Do not return contact alias activity on the contact manager page"
This reverts commit 45891bed36.

# Conflicts:
#	app/dashboard/views/alias_contact_manager.py
2021-07-14 17:21:07 +02:00
Son Nguyen Kim
25f0a71ea5 add 2 weeks mention on global stats 2021-07-14 17:15:46 +02:00
Son Nguyen Kim
ba6b6e2fdd Delete EmailLog older than 2 weeks 2021-07-14 17:15:28 +02:00
Son Nguyen Kim
253be7bad4 Revert "disable global stats to help the DB"
This reverts commit 7ce83c36b9.
2021-07-14 15:55:03 +02:00
Son Nguyen Kim
189eb8427e speed up should_disable() 2021-07-14 12:25:44 +02:00
Son Nguyen Kim
e26287a4c7 Revert "disable should_disable() for now"
This reverts commit fb88654d84.
2021-07-14 12:23:02 +02:00
Son Nguyen Kim
b98e913304 fix contact page 2021-07-13 22:25:53 +02:00
Son Nguyen Kim
45891bed36 Do not return contact alias activity on the contact manager page 2021-07-13 19:27:45 +02:00
Son Nguyen Kim
fb88654d84 disable should_disable() for now 2021-07-13 17:24:28 +02:00
Son Nguyen Kim
7ce83c36b9 disable global stats to help the DB 2021-07-13 14:52:02 +02:00
Son Nguyen Kim
80d23b8c4f always enable flask_debugtoolbar when local run 2021-07-13 14:15:49 +02:00
Son Nguyen Kim
eb8118e89e split in trunks for fill-up-email-log-alias 2021-07-13 14:15:33 +02:00
Son Nguyen Kim
8583615ba1 logging time for each request 2021-07-13 14:14:40 +02:00
Son Nguyen Kim
cbd6c96d01 preload Alias.hibp_breaches 2021-07-13 14:11:27 +02:00
Son Nguyen Kim
7ac2a02b27 join with EmailLog directly without passing by Contact 2021-07-13 14:11:14 +02:00
Son Nguyen Kim
4b9b3f18a2 add index for contact.reply_email col 2021-07-13 10:17:10 +02:00
Son Nguyen Kim
7c65d92cc1 better logging 2021-07-13 09:59:59 +02:00
Son Nguyen Kim
edab5dfac3 fix 2021-07-13 09:59:51 +02:00
Son Nguyen Kim
19c067fa17 add coupon admin 2021-07-13 08:55:04 +02:00
Son Nguyen Kim
601385a0c1 add coupon 2021-07-13 08:54:37 +02:00
Son Nguyen Kim
7a8b5d80ed Create coupon model 2021-07-12 19:26:28 +02:00
Son Nguyen Kim
cc650f9fae remove unused import 2021-07-12 18:56:43 +02:00
Son Nguyen Kim
99599bb09f make sure user needs to go through MFA when resetting password 2021-07-12 18:56:09 +02:00
Son Nguyen Kim
c011a4b90b remove unused import 2021-07-11 15:05:31 +02:00
Son Nguyen Kim
183449e38b fix test 2021-07-11 15:00:47 +02:00
Son Nguyen Kim
1e4746dfe5 fix test 2021-07-11 15:00:22 +02:00
Son Nguyen Kim
b01fd18951 Add "flask fill-up-email-log-alias" command 2021-07-11 12:29:10 +02:00
Son Nguyen Kim
f97b18e60a fill up EmailLog.alias_id when creating new EmailLog 2021-07-11 12:28:42 +02:00
Son Nguyen Kim
0b063cb409 Add EmailLog.alias_id column 2021-07-11 12:27:30 +02:00
Son Nguyen Kim
fe1f8e9eb8 make the reverse-alias replacement visible 2021-07-11 09:07:25 +02:00
Son Nguyen Kim
7ae60b9d82 alert phishing attempt 2021-07-11 08:40:10 +02:00
Son NK
f59651045d use data-bouncer-message instead of title to display error message 2021-07-03 17:50:54 +02:00
Son NK
95d6fa3478 make sure user can create new alias to receive an alias transfer 2021-07-03 17:12:03 +02:00
Son
9a9da53a58 update email wording for the one click unsubscribe 2021-06-28 16:38:47 +02:00
Son NK
3443b456b5 add sql migration 2021-06-27 17:51:28 +02:00
Son NK
01815b9153 replace get_suffix() by User.get_random_alias_suffix() 2021-06-27 17:51:13 +02:00
Son NK
09d00df363 reformat imports 2021-06-27 17:50:36 +02:00
Son Nguyen Kim
68c1463707
Merge pull request #473 from developStorm/master
Able to config random suffix
2021-06-27 12:07:11 +02:00
Raymond Nook
4469a64de6
Merge branch 'simple-login:master' into master 2021-06-24 02:57:17 -07:00
Son NK
6532e0de93 Return 550 instead of 421 when rate limited. Rename greylisting to rate limit 2021-06-24 09:47:01 +02:00
Son NK
6d67c02311 refactor 2021-06-23 19:57:21 +02:00
Son NK
e8cee6de80 increase greylisting threshold 2021-06-23 19:55:41 +02:00
Son NK
70b51b5002 return 550 instead of 421 in case of SMTPRecipientsRefused 2021-06-23 19:50:42 +02:00
Son NK
945496f67d use warning for ignore email 2021-06-23 19:47:51 +02:00
Son NK
6fa267e92b refactor: put all SMTP statuses into status.py 2021-06-23 19:47:06 +02:00
Son NK
58a1d6e783 add warning for when postfix queue id can't be retrieved 2021-06-23 18:19:13 +02:00
snyk-bot
57655fad8a
fix: upgrade vue from 2.6.12 to 2.6.13
Snyk has created this PR to upgrade vue from 2.6.12 to 2.6.13.

See this package in npm:
https://www.npmjs.com/package/vue

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2021-06-23 03:33:26 +00:00
Son NK
b84eb13ab5 Discard ignored email
Create IgnoredEmail model
2021-06-22 17:52:24 +02:00
Son NK
3ed9c3d6fe remove uptimerobot from README 2021-06-22 11:40:35 +02:00
Son
ec763544f1 handle 429 returned by HIBP 2021-06-19 19:56:18 +02:00
Son
38de6118ee remove the restart trial option 2021-06-18 09:32:46 +02:00
Raymond Nook
101c6c85ef
Merge branch 'simple-login:master' into master 2021-06-17 20:19:01 -07:00
Son
6bc093df3f fix test 2021-06-17 23:54:14 +02:00
Son
e14e697207 fix flake8 2021-06-17 23:24:07 +02:00
Son
2bed79095c ignore 5** error from HIBP 2021-06-17 23:04:25 +02:00
Son
93991816c9 fix "Received" header is not str 2021-06-17 23:02:25 +02:00
Raymond Nook
e40c276a68
Merge branch 'master' into master 2021-06-05 22:57:27 -07:00
devStorm
e79959c330
🐛 imported but unused 2021-06-05 22:53:16 -07:00
Son NK
3e1f098c79 fix test 2021-06-05 17:48:41 +02:00
Son NK
3308919906 Remove /alias/custom/new 2021-06-05 17:41:28 +02:00
Son NK
ef32998e99 Remove /alias/options, /v2/alias/options, /v3/alias/options 2021-06-05 17:41:18 +02:00
Son NK
a2ffc53c62 user can have manual subscription applied if their current subscription is canceled 2021-06-05 17:28:04 +02:00
Son NK
78df95395b improve upgrade_channel 2021-06-05 17:20:42 +02:00
Son NK
00e0b69c76 accept email sent to an alias from its mailbox 2021-06-04 17:45:51 +02:00
Son NK
79d0ef8906 Use Postfix queue-id as log message-id 2021-06-04 17:15:59 +02:00
Son NK
d53796c8d9 use warning level for icloud bounce handling 2021-06-04 15:23:48 +02:00
Son NK
10414a6b96 flake8 2021-06-02 19:04:58 +02:00
Son NK
da0424666a fix migration 2021-06-02 18:51:04 +02:00
Son NK
62683a221a black 2021-06-02 18:48:35 +02:00
Son Nguyen Kim
b14d79c8f7
Merge pull request #496 from nbraud/pw_hash/refactor
Fix minor issues with password-handling, refactor
2021-06-02 18:33:56 +02:00
Son NK
eb2adc870a make sure only premium user can create new mailbox via API 2021-06-02 17:17:28 +02:00
Son NK
dd591c7437 fix 2021-06-02 16:27:48 +02:00
Son NK
54f806fc4d handle icloud bounce 2021-06-02 11:46:00 +02:00
Son NK
3897f6b633 refactor handle_bounce() 2021-06-02 11:38:52 +02:00
Son NK
22096cae66 remove ProductHunt banner 2021-05-31 18:09:21 +02:00
Son NK
09abdffda3 remove msg logging for auto reply case 2021-05-30 20:02:41 +02:00
Son NK
ed938dd86a Add query2str 2021-05-30 19:58:46 +02:00
Son NK
809a50f7d1 Handle out-of-office email during forward phase 2021-05-30 19:58:08 +02:00
nicoo
586654e08e app.pw_models: Refactor, use constant-time equality 2021-05-29 17:42:46 +02:00
Son NK
28285f28ac Add index for AliasHibp 2021-05-28 19:59:26 +02:00
Son NK
c890bfb073 increase HIBP sleep time to have some marges 2021-05-28 17:47:54 +02:00
Son NK
1750ad45d5 fix message logging 2021-05-28 17:46:52 +02:00
Son NK
aa667851e9 log user-agent in deprecated endpoint 2021-05-28 17:46:34 +02:00
nicoo
ecd74b801b app.pw_models: Use unicode normalization
Per NIST [SP800-63B, §5.1.1.2] Memorized Secret Verifiers :
> the verifier SHOULD apply the Normalization Process for
> Stabilized Strings using either the NFKC or NFKD normalization

This is necessary for Unicode passwords to work reliably.
ASCII-only passwords aren't affected.

[SP800-63B, §5.1.1.2]: https://pages.nist.gov/800-63-3/sp800-63b.html#-5112-memorized-secret-verifiers
2021-05-27 22:16:07 +02:00
nicoo
d216812f14 tests/api/auth: Use a pw showing Unicode issues 2021-05-27 22:16:07 +02:00
devStorm
e6192ece01
style 2021-05-26 22:34:50 -07:00
Raymond Nook
258d505cbf
Merge branch 'master' into master 2021-05-26 22:33:20 -07:00
devStorm
f7bef3941a
replace random_word with get_suffix(user) 2021-05-26 22:30:12 -07:00
nicoo
f5f4d46aa4 tests/api/test_auth_login: Refactor
Have a single “login success” test, for both MFA and no-MFA cases.
No functional change to the test.
2021-05-26 19:05:26 +02:00
nicoo
52d4d2abdb app.models: minor refactor (extract pw auth) 2021-05-26 18:18:47 +02:00
Son NK
8cfd5e01dc add alerts on /alias/custom/new and /v3/alias/options and below 2021-05-25 19:36:45 +02:00
Son NK
99d26a01cb UI tweak 2021-05-25 18:30:14 +02:00
Son NK
12f3901330 use same footer as landing page 2021-05-25 18:29:55 +02:00
Son NK
388a425cac Only show pagination control if there are previous/next page 2021-05-25 18:27:06 +02:00
Son NK
b23e3d94fd make sure AliasHibp has cascade ondelete 2021-05-25 18:14:44 +02:00
Son NK
fb97f384e4 small UI tweak 2021-05-25 17:59:40 +02:00
Son NK
60a1f48e6e take into account BOUNCE_PREFIX_FOR_REPLY_PHASE when handling bounces 2021-05-25 17:59:40 +02:00
Son NK
73555ad524 generate mail_from during reply phase using BOUNCE_PREFIX_FOR_REPLY_PHASE 2021-05-25 17:59:40 +02:00
Son NK
2f96322977 make sure BOUNCE_PREFIX_FOR_REPLY_PHASE can't be used as directory name or for creating aliases on-the-fly 2021-05-25 17:59:40 +02:00
Son NK
a918cc3670 Add BOUNCE_PREFIX_FOR_REPLY_PHASE 2021-05-25 17:59:40 +02:00
Son Nguyen Kim
8262d3559d
Merge pull request #483 from simple-login/chore/remove-sudo-docker
chore: remove sudo in running docker
2021-05-25 16:33:34 +02:00
Son NK
3c6c3f7dbd add log when creating a new EmailLog 2021-05-24 12:08:30 +02:00
Son NK
159843a923 Add log for sl_sendmail 2021-05-24 12:04:22 +02:00
Son NK
bdec7ff5e4 use info level for case user is deleted in the meantime 2021-05-24 11:10:17 +02:00
doanguyen
4db8a4169e chore: remove sudo in running docker
Running docker in `sudo` mode is considered harmful.
It's recommended to run docker as non-root user to
minimize the security risks.

[0]: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user
2021-05-22 22:06:47 +02:00
Son NK
ce22e16285 add logging for case reverse alias receiving email from <> 2021-05-22 17:11:32 +02:00
Son NK
ade07f9449 return empty name when name can't be decoded 2021-05-22 16:47:44 +02:00
Son NK
78e3a4bf77 handle the case an alias is deleted in the meantime 2021-05-22 16:36:19 +02:00
Son Nguyen Kim
e911bdf203
Merge pull request #480 from TheLastProject/feature/hibp_direct_link
Add direct link to HIBP pwned info
2021-05-21 15:29:46 +02:00
Sylvia van Os
1ee941647f Add direct link to HIBP pwned info 2021-05-21 12:08:00 +02:00
Son Nguyen Kim
7a1a1d3a01
Merge pull request #479 from TheLastProject/patch-1
Update hibp_last_check on succesful HIBP check
2021-05-20 19:07:50 +02:00
Sylvia van Os
6bcaa6453e
Update hibp_last_check on succesful HIBP check
Accidentally got rid of this during some refactor
2021-05-20 19:00:11 +02:00
Son NK
14bc4f8872 make sure to only run HIBP check on enabled alias 2021-05-19 16:12:58 +02:00
Son NK
3422bd9aee fix crontab 2021-05-19 15:38:46 +02:00
Son NK
d4e930c930 Remove nullsfirst as not compatible with sqlite. Add more logging 2021-05-19 12:46:55 +02:00
Son Nguyen Kim
b3f8fd6789
Merge pull request #472 from TheLastProject/feature/hibp
Add HIBP checks
2021-05-19 12:37:04 +02:00
Sylvia van Os
40d0dee88f asyncio-ify 2021-05-18 21:18:07 +02:00
Sylvia van Os
a08b0c05cc Don't override id 2021-05-17 21:29:29 +02:00
Sylvia van Os
969616d671 Date compare in DB instead of model function 2021-05-17 18:20:35 +02:00
Son NK
33f70914fa improve PH hello bar 2021-05-17 18:08:57 +02:00
Son Nguyen Kim
c7f6e6cedb
Merge pull request #474 from PeterDaveHello/patch-1
Enable nginx config block syntax highlight in README.md
2021-05-17 14:39:11 +02:00
Peter Dave Hello
a9794325cd
Enable nginx config block syntax highlight in README.md 2021-05-17 18:56:38 +08:00
Sylvia van Os
a9c897c6c5 Fix typo 2021-05-16 00:10:04 +02:00
Sylvia van Os
42cfce7ce1 Optimize API requests on multiple API keys 2021-05-15 23:23:59 +02:00
Sylvia van Os
bee468e055 Black 2021-05-15 18:04:50 +02:00
devStorm
4a0fc8380f
variable naming 2021-05-14 11:03:16 -07:00
Sylvia van Os
b3fa445250 Set up HIBP cron (max 1 at a time) 2021-05-14 19:57:57 +02:00
Sylvia van Os
f67f5297f2 Add HIBP checks 2021-05-14 19:50:32 +02:00
Son NK
ef2eb7f959 add Paddle subscription_id in admin 2021-05-14 16:10:34 +02:00
devStorm
30183ac8c3
🐛 fix style 2021-05-13 19:34:54 -07:00
devStorm
5c74ad2dc0
⚠️ Remove word list check 2021-05-13 16:55:46 -07:00
devStorm
178ce34399
Enum, setting 2021-05-13 16:53:01 -07:00
devStorm
3fc250018d
basic implementation of random suffix 2021-05-13 16:13:19 -07:00
Son NK
cb3bc8bc36 change PH banner wording 2021-05-13 00:54:36 +02:00
Son NK
078f3e8188 Set the "X-SimpleLogin-Envelope-To" to the alias during forward 2021-05-12 10:46:07 +02:00
Son NK
6801b1f453 add PH banner 2021-05-11 20:15:07 +02:00
Son NK
edaf293398 Create admin pages for Referral and Payout 2021-05-10 15:51:39 +02:00
Son NK
93a8873192 show payouts on referral page 2021-05-10 13:03:44 +02:00
Son NK
229d2c644b Add Payout model 2021-05-10 13:03:19 +02:00
Son
ac2ee4f2d0 remove analytics 2021-05-07 23:55:06 +02:00
Son NK
ecdef797f9 generate a message_id at the beginning of email processing 2021-05-06 17:20:33 +02:00
Son NK
c003dd0b01 create a copy msg for every recipient except the last one 2021-05-06 17:08:30 +02:00
Son NK
5317b8ab84 move the api key page back to menu 2021-05-05 18:51:15 +02:00
Son NK
e5926978c8 use bouncer on custom alias page 2021-04-30 11:45:00 +02:00
Son NK
722e38deb1 install formbouncer 2021-04-30 11:44:43 +02:00
Son NK
ceacf8e3a7 support dot in alias prefix 2021-04-30 11:37:17 +02:00
Son NK
e74dbd7e98 increase monthly plan to $4 2021-04-23 12:08:27 +02:00
Son NK
c5697fbf3c remove unused import 2021-04-23 12:07:49 +02:00
Son NK
7946879308 schedule account deletion 2021-04-23 11:50:26 +02:00
Son NK
f3b04b9d81 add more logging 2021-04-16 18:37:16 +02:00
Son NK
0039b4c301 disable an alias if the user has too many bounces recently 2021-04-16 17:57:25 +02:00
Son NK
45b0acc1c4 remove unused import 2021-04-12 20:21:25 +02:00
Son NK
15610f1efc Run migrate_domain_trash() and set_custom_domain_for_alias() in sanity check 2021-04-12 20:20:55 +02:00
Son NK
cfb52a2eba add price mention on billing page 2021-04-12 10:14:35 +02:00
Son NK
5040e7b74b add filters for alias and mailbox admin 2021-04-12 10:07:17 +02:00
Son NK
9bfd9ebf07 fix 2021-04-09 12:46:51 +02:00
Son NK
a1de682ae1 update email wording 2021-04-09 12:46:28 +02:00
Son NK
62c36a6e22 update email wording 2021-04-09 12:43:30 +02:00
Son NK
aad1270e0d free trial account can't create more than MAX_NB_EMAIL_FREE_PLAN aliases 2021-04-09 12:40:55 +02:00
Son NK
95c8f14ea5 update doc 2021-04-07 11:57:48 +02:00
Son NK
06bb3ffe41 v3.4.0 2021-04-07 11:56:07 +02:00
Son NK
f45e7b53d0 create admin for Client 2021-04-06 19:46:38 +02:00
Son NK
f8540808bc remove Client.published 2021-04-06 19:46:21 +02:00
Son NK
e42fb0816d Improve SIWSL wording 2021-04-06 18:12:06 +02:00
Son NK
ea5281de95 automatically show how-to-use 2021-04-06 18:10:32 +02:00
Son NK
7c1af6a265 improve should_disable(): take into account repetitive bounces 2021-04-06 17:24:06 +02:00
Son NK
45221477a3 update .dockerignore to match .gitignore 2021-04-06 15:19:49 +02:00
Son NK
be5cdc59ba fix test 2021-04-06 15:19:28 +02:00
Son NK
c715f87526 improve SIWSL UI 2021-04-06 12:06:46 +02:00
Son NK
9e4ff01b17 improve login, register UI 2021-04-06 12:06:11 +02:00
Son NK
17cb4462e3 fix test 2021-04-01 18:17:27 +02:00
Son NK
af9597cf5a improve copy when app isn't approved 2021-04-01 18:10:39 +02:00
Son NK
38730bdecd improve oauth doc 2021-04-01 18:10:27 +02:00
Son NK
085dec069b allow any redirect_uri if the app isn't approved 2021-04-01 18:04:45 +02:00
Son NK
03976ea1c2 improve copy in app approval 2021-04-01 18:04:35 +02:00
Son NK
9757b12b95 user can remove the app link 2021-04-01 14:20:13 +02:00
Son NK
efae1710c8 extract the app/website to a separate page 2021-04-01 14:09:16 +02:00
Son NK
3c4a1413e0 Remove ClientUser.nonce 2021-04-01 12:49:32 +02:00
Son NK
e6d8815ac5 take into account nonce in openid 2021-04-01 12:49:23 +02:00
Son NK
3c5706fb16 only run app without ssl 2021-04-01 12:37:05 +02:00
Son NK
2f28e51c53 missing migration 2021-04-01 12:35:43 +02:00
Son NK
da17f51778 add AuthorizationCode.nonce 2021-04-01 12:35:21 +02:00
Son NK
313b442af7 do not display AppId 2021-04-01 12:32:14 +02:00
Son NK
36e7cf3fdc add ClientUser.nonce 2021-04-01 12:31:37 +02:00
Son NK
46109770fc prettify 2021-04-01 11:05:58 +02:00
Son NK
b13c65166f Add OpenID Connect Discovery Document URL 2021-04-01 11:05:21 +02:00
Son NK
a90fa49636 add submit for approval for app 2021-04-01 10:52:51 +02:00
Son NK
c3d57ed6e4 reformat code 2021-04-01 10:50:53 +02:00
Son NK
7d4e1048af show warning on authorize page for non-approved app 2021-04-01 10:50:37 +02:00
Son NK
bbfb69d774 Add Client approved, description columns 2021-04-01 10:50:11 +02:00
Son NK
eab4f5f7ac prettify app page 2021-04-01 10:02:08 +02:00
Son NK
d5de99afe9 Restore /alias/custom/new as currently used by safari 2021-03-31 14:41:32 +02:00
Son NK
e31e19047c improve admin 2021-03-30 19:08:41 +02:00
Son Nguyen Kim
9df62e0380
Merge pull request #421 from froozeify/readme-missing-value
Missing DKIM_PUBLIC_KEY_PATH in readme and example.env
2021-03-30 18:31:58 +02:00
Benoit VIGNAL
f9366e2ed4 doc: add missing DKIM_PUBLIC_KEY_PATH in readme and example.env 2021-03-29 19:57:19 +02:00
Son NK
1b41911598 remove social login from the login page 2021-03-29 16:06:58 +02:00
Son NK
ac216e7a08 use create_light_app in job runner 2021-03-29 10:56:42 +02:00
Son NK
1f4637c064 add logging for message id 2021-03-29 10:27:19 +02:00
Son NK
bbf895ed42 add alias creation report to stats 2021-03-29 10:10:25 +02:00
Son NK
9d5c2e3f80 display when subscription ends in admin 2021-03-26 12:14:48 +01:00
Son NK
26a087619c support extend manual subscription in admin 2021-03-26 12:14:33 +01:00
Son NK
365c11f926 only run spam check in email handler if ENABLE_SPAM_ASSASSIN is enabled 2021-03-26 10:00:48 +01:00
Son NK
2270ccf35d Add ENABLE_SPAM_ASSASSIN setting 2021-03-26 10:00:16 +01:00
Son NK
0bb8f9a227 make ALIAS_LIMIT configurable 2021-03-26 09:56:04 +01:00
Son NK
5a55121dfc Remove Trello 2021-03-25 19:21:21 +01:00
Son NK
1cac625a90 restore /v2/alias/custom/new as used by browser extension 2021-03-25 19:18:50 +01:00
Son NK
c122b05896 refactor cron 2021-03-24 17:39:16 +01:00
Son NK
8eed6008f3 fix cron 2021-03-24 17:38:17 +01:00
Son NK
92acf352b6 add limiter for random alias creation 2021-03-24 17:30:05 +01:00
Son NK
a570a426d4 remove --cov option from pytest to allow debugging in pycharm 2021-03-24 17:25:41 +01:00
Son NK
71389b7e09 add limiter on custom alias page 2021-03-24 16:52:05 +01:00
Son NK
e46e3b1c01 fix test 2021-03-24 16:51:23 +01:00
Son NK
acc285abf0 remove /alias/custom/new, /v2/alias/custom/new, refactor test 2021-03-24 16:39:49 +01:00
Son NK
0c62ac4b1f set rate limit for creating alias endpoint 2021-03-24 16:26:42 +01:00
Son NK
6435d951e1 refactor test 2021-03-24 16:11:22 +01:00
Son NK
10cc61b4a0 refactor test 2021-03-24 15:54:03 +01:00
Son NK
84b4c11086 handle UnicodeEncodeError in encode() 2021-03-24 10:08:11 +01:00
Son NK
db04303172 Remove trello link 2021-03-23 18:47:26 +01:00
Son NK
d59cee0bcc improve welcome email 2021-03-23 18:47:16 +01:00
Son NK
1492f29a1a flake8 2021-03-23 10:50:49 +01:00
Son NK
58b0c91db5 use Metric2 system 2021-03-23 10:50:32 +01:00
Son NK
1600e273dd remove Metric2.name column 2021-03-23 10:30:57 +01:00
Son NK
ec1633d0d7 create Metric2 model 2021-03-23 10:23:40 +01:00
Son NK
e9e97cea61 send bounce report in cron 2021-03-23 10:02:14 +01:00
Son NK
f038a97649 refactor cron 2021-03-23 09:47:57 +01:00
Son NK
7ab64d9768 add more logging 2021-03-22 15:52:48 +01:00
Son NK
2ad1b75e45 change wording 2021-03-22 15:40:17 +01:00
Son NK
c0efc78a94 fix 2021-03-18 14:56:32 +01:00
Son NK
d2c99ea00e add more fake data 2021-03-18 14:45:31 +01:00
Son NK
f0fb5108f9 show a different message for custom domain alias when deleting 2021-03-18 14:45:18 +01:00
Son NK
8b234b63a5 add custom domain to AliasInfo 2021-03-18 14:44:51 +01:00
Son NK
8bb324e82b propose users to disable an alias instead of deleting it 2021-03-18 14:21:26 +01:00
Son NK
9dede0a281 remove unused var 2021-03-18 13:54:14 +01:00
Son NK
bcfc846af3 refactor: extract disableAlias() 2021-03-18 13:52:58 +01:00
Son NK
89c69ad625 refactor index.js 2021-03-18 13:52:13 +01:00
Son NK
91e805a637 move js part in index to a separate file 2021-03-18 12:34:08 +01:00
Son NK
1187b6dc99 update mailbox wording 2021-03-18 10:59:45 +01:00
Son NK
2d968a01f8 fix doc 2021-03-17 20:31:05 +01:00
Son NK
c87fe55898 POST /api/aliases/:alias_id/contacts: return 200 and existed=true if contact is already added. 2021-03-17 20:29:34 +01:00
Son NK
48d7b66803 flake8 2021-03-17 11:05:26 +01:00
Son NK
66eb93fe53 fix sanitize header 2021-03-17 10:59:13 +01:00
Son NK
0848405d0c add mention not allowing forward email address 2021-03-17 10:27:46 +01:00
Son NK
aadf2e1939 reusing the msg already sanitized 2021-03-17 10:23:35 +01:00
Son NK
5cba2eaa38 sanitize header 2021-03-17 10:23:35 +01:00
Son NK
826e4455cf refactor 2021-03-17 10:23:35 +01:00
Son NK
d0dd64bf7b change method order 2021-03-17 10:23:35 +01:00
Son NK
11789559f1 move spf_pass(), sl_sendmail() to email_utils.py 2021-03-17 10:23:35 +01:00
Son NK
d1d81e6a6d move get_spam_score_async(), get_spam_score() to email/spam.py 2021-03-17 10:23:35 +01:00
Son Nguyen Kim
f6d3172e3e
Create SECURITY.md 2021-03-16 19:44:59 +01:00
Son Nguyen Kim
169d70881a
Update bug_report.md 2021-03-16 19:12:07 +01:00
Son Nguyen Kim
89df6fc61a Update issue templates 2021-03-16 19:01:25 +01:00
Son NK
57b2e2d4ab fix handle_batch_import 2021-03-16 10:54:00 +01:00
Son NK
9a1dc0240b improve logging 2021-03-16 09:17:23 +01:00
Son NK
7ec0405709 improve message_id 2021-03-16 09:15:59 +01:00
Son NK
2d7219c218 flake8 2021-03-15 20:00:10 +01:00
Son NK
82154ec858 ignore email sent from a reverse-alias 2021-03-15 19:55:22 +01:00
Son NK
7811f06fc1 remove asyncio_main() 2021-03-15 19:49:14 +01:00
Son NK
98264b14bc replace linebreak in sanitize_email() 2021-03-15 19:42:28 +01:00
Son NK
6eb7ebc338 add message_id in log to keep track of an email processing 2021-03-15 19:41:42 +01:00
Son Nguyen Kim
ca4d097f14
Merge pull request #381 from TheLastProject/feature/importExportTests
Feature/import export tests
2021-03-15 15:17:58 +01:00
Sylvia van Os
e02d95216f Remove unused imports and var assignments 2021-03-13 15:46:12 +01:00
Sylvia van Os
cfe889f7b9 Format with black 2021-03-13 15:37:28 +01:00
Sylvia van Os
8800c29526 Complete import tests 2021-03-13 15:36:25 +01:00
Sylvia van Os
dcfd63eb0f Working import tests 2021-03-13 00:13:33 +01:00
Sylvia van Os
44ae20816a WIP: Import test 2021-03-10 23:08:33 +01:00
Son
cf6442cec2 Add PATCH /api/custom_domains/:custom_domain_id 2021-03-10 22:56:12 +01:00
Son
eb22a6302e rename creation -> deletion for GET /api/custom_domains/:custom_domain_id/trash 2021-03-10 22:39:33 +01:00
Son
6c2daf1bb6 add mailboxes to GET /api/custom_domains 2021-03-10 22:37:26 +01:00
Sylvia van Os
c6646d5971 Add export test 2021-03-10 22:35:04 +01:00
Son Nguyen Kim
6b79dbdd5c
Merge pull request #380 from TheLastProject/feature/export_import_alias
Export and import mailbox info with alias
2021-03-10 17:05:49 +01:00
Sylvia van Os
1915c8d09d Export and import mailbox info with alias 2021-03-09 21:09:58 +01:00
Son NK
3c8ec8fcf2 make DKIM_PRIVATE_KEY_PATH optional 2021-03-08 15:18:03 +01:00
Son NK
819738f55c remove unused DKIM_PUBLIC_KEY_PATH, DKIM_DNS_VALUE 2021-03-08 15:14:37 +01:00
Son NK
58df3442d5 simplify readme 2021-03-08 15:08:41 +01:00
Son NK
48a16a1d2e use another hero image 2021-03-08 14:59:39 +01:00
Son NK
ffb7cdbdec Add Sylvia van Os to contributor 2021-03-08 14:59:39 +01:00
Son Nguyen Kim
8e81b2ed8b
Merge pull request #376 from simple-login/snyk-upgrade-e9de3bb9ab4be91ed2eae710638612b0
[Snyk] Upgrade @sentry/browser from 5.27.6 to 5.30.0
2021-03-08 14:54:25 +01:00
Son NK
cb3c5d7f12 improve contributing.md 2021-03-08 14:53:53 +01:00
Son NK
3512cc087e remove unused import 2021-03-08 14:53:14 +01:00
Son NK
9c4a5fc734 run black via poetry in CI 2021-03-08 13:39:20 +01:00
Son Nguyen Kim
095e2ae0de
Merge pull request #377 from TheLastProject/feature/api_export
Add export endpoints
2021-03-08 12:18:28 +01:00
Son NK
157b7adbda improve logging 2021-03-08 12:11:47 +01:00
Son NK
26613cdeeb fix handling auto reply 2021-03-08 12:09:27 +01:00
Son NK
bc69e11f9b enable color log when dev 2021-03-08 12:09:10 +01:00
Son NK
796ad58dca improve logging 2021-03-08 12:08:23 +01:00
Son NK
3768429909 use log format that allows clickable link to code source in PyCharm 2021-03-08 12:07:45 +01:00
Sylvia van Os
3932ed2eb8 Add export endpoints 2021-03-06 21:56:42 +01:00
Son NK
39c92110cb add more fields into /api/custom_domains 2021-03-06 20:43:50 +01:00
Son
c4c29dfa1d flake8 2021-03-06 18:13:59 +01:00
Son
863d8dcbe7 black 2021-03-06 18:10:41 +01:00
Son
178d2c8689 recreate migration file 2021-03-06 18:10:34 +01:00
Son
23a0861790 Improve alias transfer. Use alias transfer_token. Add a limiter on /alias_transfer/receive 2021-03-06 18:08:42 +01:00
Son
29afc1b6b5 Add Alias.transfer_token col 2021-03-06 18:08:42 +01:00
Son
475eaa2bc0 inform user when his alias has been transferred 2021-03-06 18:08:42 +01:00
Son NK
3494f314bc disable flask toolbar by default 2021-03-06 17:44:46 +01:00
Son NK
31ff8b962b handle the auto responder email 2021-03-06 17:44:46 +01:00
Son NK
9492aaccf5 Add EmailLog.auto_replied col 2021-03-06 17:44:46 +01:00
Son
d933bffa2f improve contributing 2021-03-06 16:30:41 +01:00
Son
2c46097330 improve contributing.md, comment all optional options in example.env 2021-03-06 16:28:15 +01:00
Son
6f37bf858d take into account NOT_SEND_EMAIL in sl_sendmail 2021-03-06 16:23:19 +01:00
Son
80d80657d6 remove cloudwatch params 2021-03-06 16:09:27 +01:00
Son
fb24760039 move contribution guide to CONTRIBUTING.md 2021-03-06 15:55:18 +01:00
snyk-bot
1386afe545
fix: upgrade @sentry/browser from 5.27.6 to 5.30.0
Snyk has created this PR to upgrade @sentry/browser from 5.27.6 to 5.30.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2021-03-06 03:23:44 +00:00
Son NK
193f8d8ccc Handle UnicodeDecodeError in get_header_unicode() 2021-03-05 20:41:23 +01:00
Son NK
3af98026e3 refactor config: do not use eval() 2021-03-05 17:26:47 +01:00
Son NK
a65680b5ba remove DEBUG param 2021-03-05 17:26:47 +01:00
Son Nguyen Kim
48dcd634fa
Merge pull request #352 from simple-login/snyk-upgrade-d5185be84c60a61c96600ab52a66962e
[Snyk] Upgrade bootbox from 5.5.1 to 5.5.2
2021-03-05 13:13:29 +01:00
Son NK
2b811f942d 3.3.0 2021-03-05 13:11:47 +01:00
Son NK
a536a34a0b remove unused import 2021-03-01 18:46:15 +01:00
Son NK
38790fdc84 get_alias_infos_with_pagination_v3: handle the case where an alias has 2 contacts that have no activity 2021-03-01 18:45:15 +01:00
Son NK
3a142ca2f8 remove .venv 2021-03-01 18:22:52 +01:00
Son NK
36a117d790 rename 2021-03-01 18:22:39 +01:00
Son NK
deef432c58 add extend trial on user admin page 2021-02-25 10:09:02 +01:00
Son NK
9ccfed28c5 fix migration: set include_sender_in_reverse_alias to false if null 2021-02-24 15:54:57 +01:00
Son NK
3aeaf6fe29 make include_sender_in_reverse_alias non nullable 2021-02-24 15:44:50 +01:00
Son NK
c9b246259d can search on manual subscription admin page 2021-02-24 09:49:08 +01:00
Son NK
0f9cb13920 add admin page for manual subscription 2021-02-24 09:48:06 +01:00
Son NK
b00524e74f upgrade flask-admin to 1.5.7, use bootstrap4 for admin UI 2021-02-24 09:47:48 +01:00
Son NK
a2dad50d20 take into account apple sub 2021-02-23 19:40:40 +01:00
Son NK
d4ac2da96a set some fields back to default 2021-02-17 13:08:02 +01:00
Son NK
9030d8b543 flake8 2021-02-17 13:01:28 +01:00
Son NK
9e486fc2c0 add alias transfer 2021-02-17 12:56:28 +01:00
Son NK
1f7366c07c enrich fake data 2021-02-17 12:49:56 +01:00
Son NK
e19fff3a9a Add Alias.original_owner_id column 2021-02-17 12:49:47 +01:00
Son NK
cb58adc44b prettify alert-info 2021-02-15 20:21:55 +01:00
Son NK
7efce95145 add warning for cloudflare when setup DNS 2021-02-15 20:21:55 +01:00
Son NK
219703cb04 replace monero by crypto 2021-02-15 16:40:19 +01:00
Son NK
7879b854a3 refactor 2021-02-15 16:39:23 +01:00
Son NK
e7063b6514 highlight reddit in welcome mail 2021-02-12 13:03:31 +01:00
Son NK
dc29c6f9d9 improve wording 2021-02-12 13:03:31 +01:00
Son
cd3854561a improve logging 2021-02-06 17:31:06 +01:00
Son
3f7d325e6e remove the exception logging when a transaction isn't found 2021-02-06 16:00:32 +01:00
Son
51a38be070 Add charity organization upgrade on admin 2021-02-03 18:54:42 +01:00
Son Nguyen Kim
33ae42dddf
Merge pull request #365 from simple-login/refactor/stable-apt
refactor(build): remove dependencies in production
2021-02-02 18:13:45 +01:00
VD
d6504b7e13
refactor(build): remove dependencies in production 2021-02-02 10:20:08 +01:00
Son NK
25afe4831c ignore amazonses.com encoding 2021-01-31 11:52:55 +01:00
Son NK
5f0930b291 handle header is None in get_header_unicode 2021-01-31 11:50:41 +01:00
Son NK
00fde00d53 black 2021-01-28 18:10:29 +01:00
Son NK
f8dbb50552 Merge branch 'staging' 2021-01-28 17:26:16 +01:00
Son NK
2831cd04d8 delete all headers in forward phase 2021-01-28 17:26:02 +01:00
Son NK
4f5a2cc8be fix 2021-01-28 16:34:24 +01:00
Son NK
40e4d8e232 Use POSTFIX_PORT_FORWARD during forward phase 2021-01-28 13:50:24 +01:00
Son NK
bbb6049351 Add POSTFIX_PORT_FORWARD 2021-01-28 13:49:40 +01:00
Son NK
b476e207fa take into account ?next param in login 2021-01-27 10:11:48 +01:00
Son NK
e651e70d2d add a bounce to fake_data() 2021-01-27 10:09:44 +01:00
Son NK
66f3585253 Add LifetimeCoupon admin page 2021-01-27 10:08:49 +01:00
Son NK
e2f729206e replace the iframe video by a link 2021-01-27 09:45:42 +01:00
Son NK
5bb0ae0234 Set user.sender_format_updated_at when user updates sender_format 2021-01-26 20:14:13 +01:00
Son NK
0354943ff4 Add User.sender_format_updated_at column 2021-01-26 20:13:51 +01:00
Son NK
01aa733fe8 fix test 2021-01-26 20:06:39 +01:00
Son NK
e85cfebf92 do not show sender format that has full sender address 2021-01-26 19:55:56 +01:00
Son NK
6547d9420f Use "John Wick - john at wick.com" as default sender format 2021-01-26 19:54:59 +01:00
Son NK
d3a825c44b upgrade sentry 2021-01-26 19:19:05 +01:00
Son NK
3544db8f1c sanitize contact.website_email in bounce 2021-01-26 10:04:03 +01:00
Son NK
4cd49b66c2 use VERP for transactional email: remove SENDER, SENDER_DIR 2021-01-26 09:59:22 +01:00
Son NK
3e1ef3358b Create bounce when handling bounce 2021-01-26 09:59:22 +01:00
Son NK
b3181c054f make sure to delete bounces, transactional emails after 7 days 2021-01-26 09:59:22 +01:00
Son NK
1013e8dd79 Create Bounce, TransactionalEmail models 2021-01-26 09:59:22 +01:00
Son NK
e09e6c51b8 make sure all metrics have the same date 2021-01-26 09:59:22 +01:00
Son NK
4a91db8e11 rename parse_email_log_id_from_bounce -> parse_id_from_bounce 2021-01-26 09:59:22 +01:00
Son
fcc04ba929 handle case where email_log is deleted 2021-01-25 21:27:34 +01:00
Son
81840e5ba5 do not use user.name in email 2021-01-25 21:25:15 +01:00
Son NK
113dbd1c81 fix nb_referred_user_upgrade computation 2021-01-25 18:50:08 +01:00
Son NK
2a33f112b9 compute metrics 2021-01-25 18:49:14 +01:00
Son NK
980942a1f9 create Metric model 2021-01-25 18:47:02 +01:00
Son NK
0de5b5a9bf revert change 2021-01-25 18:45:38 +01:00
Son NK
604ba285b1 replace "550 SL E3" by "550 5.1.1 SL E3 " 2021-01-25 17:34:41 +01:00
Son NK
b0b74906a7 add confirm before changing the plan 2021-01-25 10:45:18 +01:00
Son NK
2bcd238250 Add hint for generic subject 2021-01-20 19:17:34 +01:00
Son NK
8e49fc40d4 fix 2021-01-20 13:28:23 +01:00
Son NK
202f28722e make MAX_SPAM_SCORE, MAX_REPLY_PHASE_SPAM_SCORE configurable 2021-01-20 13:27:30 +01:00
Son NK
013a94d1e9 fix user delete profile pic 2021-01-19 10:47:48 +01:00
Son NK
da53b7fa00 Improve should_disable() to take into account last 7 days bounces 2021-01-19 10:45:39 +01:00
Son NK
0d6338b525 fix disable alias email subject 2021-01-19 10:36:11 +01:00
Son NK
f94b82c134 remove unused import 2021-01-19 10:33:20 +01:00
Son NK
d1d7a93ca5 remove handle_bounce_deprecated 2021-01-19 10:27:57 +01:00
Son NK
c927edfeaa add a dummy profile pic for fake data 2021-01-19 09:15:55 +01:00
Son NK
7b9136d951 take into account user.profile_picture can be None 2021-01-19 09:15:43 +01:00
Son NK
264f41d466 Fix Paddle modal not showed up on mobile 2021-01-18 17:00:58 +01:00
Son
e75ede969a Ctrl-enter submit the form on custom alias page 2021-01-16 19:56:30 +01:00
Son
8a74aee363 black 2021-01-16 10:55:14 +01:00
Son
f9161dba20 Handle "message/rfc822" in replace() 2021-01-16 10:45:50 +01:00
Son NK
fd0ba7030d Add the condition of at least 3 months to referral program 2021-01-15 17:26:05 +01:00
Son NK
7986ff0819 Handle the case msg.get_all return Header object (and not string) 2021-01-15 11:30:43 +01:00
Son NK
4bfe6d1ac9 handle name can be None in get_name_initial() 2021-01-15 11:21:45 +01:00
Son NK
f4218a0693 Log exception when handle_bounce_deprecated is used 2021-01-13 11:03:44 +01:00
Son NK
c431abd917 take into account alias.cannot_be_disabled in should_disable 2021-01-13 11:03:30 +01:00
Son NK
79f22857b5 add mailbox admin page 2021-01-12 18:14:35 +01:00
Son NK
67de0e3c5b set user as default admin page 2021-01-12 18:14:21 +01:00
Son NK
f9b0bdc2ed Remove mentions of AWS, Sentry, Google/Facebook login in self hosting instructions 2021-01-12 09:54:46 +01:00
Son NK
d6cc2a4bf3 Add AWS_REGION env var 2021-01-12 09:54:46 +01:00
Son Nguyen Kim
7cf0bb4ef4
Merge pull request #359 from AndreasGassmann/patch-1
Fix "Postgress" typos in readme
2021-01-12 09:43:48 +01:00
AndreasGassmann
79e0fcf3d4
fix typos in readme 2021-01-12 08:07:49 +01:00
Son NK
361945f3f8 add cash and monero upgrade in admin 2021-01-11 20:24:33 +01:00
Son NK
56864ff0df improve log 2021-01-11 15:53:08 +01:00
Son NK
0e94c329d1 handle alias too long error 2021-01-11 15:45:41 +01:00
Son NK
8764a050d5 fix orig email not correctly uploaded in handle_bounce_forward_phase() 2021-01-11 15:25:54 +01:00
Son NK
a044c47295 fix 2021-01-11 15:10:46 +01:00
Son NK
13f3deb671 fix 2021-01-11 15:06:56 +01:00
Son NK
f1e9b2b5d7 use VERP: send email from bounce address 2021-01-11 14:55:55 +01:00
Son NK
c83b146f14 Add BOUNCE_PREFIX, BOUNCE_SUFFIX config 2021-01-11 14:51:29 +01:00
Son NK
45ac548e2b reserve bounce, bounces as directory name 2021-01-11 14:32:48 +01:00
Son NK
2cc7f5ac13 only alert for contact that's created after the sanity date 2021-01-11 12:35:54 +01:00
Son NK
0ee0167b8e fix test 2021-01-11 12:31:05 +01:00
Son NK
e9adb3270d use sanitize_email instead of .lower().strip().replace(" ", "") 2021-01-11 12:29:40 +01:00
Son NK
01858ac452 sanitize contact email 2021-01-11 12:27:02 +01:00
Son NK
2293c6d2e3 improve admin 2021-01-11 12:04:37 +01:00
Son NK
70cc920ce8 flake8: Ignore "f-string is missing placeholders" 2021-01-11 10:28:29 +01:00
Son NK
cce08adb87 set "" as default name when creating new user 2021-01-11 10:25:37 +01:00
Son NK
435ced66bc make User.name nullable 2021-01-11 10:24:00 +01:00
Son NK
ef7fae32b1 remove the "Hi {name}" from email template 2021-01-11 10:23:34 +01:00
Son NK
c9c2190874 upgrade package in poetry 2021-01-10 20:24:09 +01:00
Son NK
08d8e11a27 flake8 2021-01-08 18:58:14 +01:00
Son NK
4293bba5ab add edu upgrade to User admin page 2021-01-08 18:53:01 +01:00
Son NK
7e53b97f81 handle the case where alias is deleted in handle_bounce_reply_phase() 2021-01-04 19:25:15 +01:00
Son NK
271734f5e2 fix Firefox link 2021-01-04 15:13:38 +01:00
Son NK
3ac159d073 use text/plain for text email instead of text/text 2021-01-04 15:11:12 +01:00
Son NK
30593f9c78 store spam report 2021-01-04 14:43:57 +01:00
Son NK
8dbaf3cf56 Add EmailLog.spam_report column 2021-01-04 14:38:32 +01:00
Son NK
b942b44ec8 display bounce during reply phase on alias log page 2021-01-04 14:25:51 +01:00
Son NK
aae63006c6 handle bounce report sent to alias 2021-01-04 14:22:07 +01:00
Son NK
2b4dc3cdcc when a custom domain is default and has random_prefix_generation enabled, use the random_prefix as the first choice 2021-01-02 18:17:53 +01:00
Son NK
3179d70df1 ignore text/csv in replace() 2020-12-31 18:03:42 +01:00
Son NK
a34af98de8 handle "multipart/signed" in add_header() 2020-12-31 15:50:03 +01:00
Son NK
ef2624ccea handle multipart/mixed in add_header 2020-12-31 15:11:46 +01:00
Son NK
0cf283089d fix migration name too long 2020-12-31 14:46:38 +01:00
Son NK
c252665e46 flake8 2020-12-31 14:40:43 +01:00
Son NK
33dd6083c7 rename available_suffixes_more_info -> get_available_suffixes 2020-12-31 14:27:04 +01:00
Son NK
95f3db6aa5 remove available_suffixes() 2020-12-31 14:26:07 +01:00
Son NK
5a3b79b4cf replace available_suffixes by available_suffixes_more_info 2020-12-31 14:25:44 +01:00
Son NK
7915a2abb9 Fix available_suffixes_more_info 2020-12-31 14:18:49 +01:00
Son NK
abb3ec1f05 rename 2020-12-31 14:16:32 +01:00
Son NK
4e20ffcc60 update wording: use custom alias to be opposed to random alias 2020-12-31 14:15:25 +01:00
Son NK
75d1b090cd rename default_random_alias_public_domain_id -> default_alias_public_domain_id 2020-12-31 14:14:56 +01:00
Son NK
e0a414212e rename default_random_alias_domain_id -> default_alias_custom_domain_id 2020-12-31 14:06:32 +01:00
Son NK
1647a7a628 update settings wording 2020-12-31 14:00:21 +01:00
Son NK
b9d8f11f2d put the default domain to top 2020-12-31 13:59:03 +01:00
Son NK
66e7aa7242 refactor 2020-12-31 12:50:04 +01:00
Son NK
291b9a7d55 use warning level for reply message detected as spam 2020-12-31 11:30:53 +01:00
Son NK
01da9aafcd retry get_spam_score 1 more time 2020-12-31 11:26:12 +01:00
Son NK
1c22e14f68 SMTPServerDisconnected can also happen when creating SMTP server object 2020-12-31 11:22:45 +01:00
Son NK
0b83835065 fix load_pgp_public_keys() 2020-12-31 11:21:54 +01:00
Son NK
0585ba97ee use warning level for "Cannot encrypt using the imported key" error 2020-12-31 11:05:11 +01:00
Son NK
74a63db835 ignore "text/directory" in replace() 2020-12-30 09:48:58 +01:00
Son NK
0df4d1a93d add more logging 2020-12-29 12:17:24 +01:00
Son NK
3b850f6228 create analytics.js 2020-12-28 16:30:44 +01:00
Son NK
07febc9715 replace GoatCounter by Plausible 2020-12-28 16:27:42 +01:00
Son NK
31774f9ea7 set include_sender_in_reverse_alias to False by default 2020-12-28 10:28:05 +01:00
snyk-bot
d924dc1d52
fix: upgrade bootbox from 5.5.1 to 5.5.2
Snyk has created this PR to upgrade bootbox from 5.5.1 to 5.5.2.

See this package in npm:
https://www.npmjs.com/package/bootbox

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-12-22 03:23:42 +00:00
Son NK
e06f3dc209 fix template 2020-12-21 11:57:12 +01:00
Son NK
6441c22bcd use warning level for email and mailbox same domain error. Update email wording. 2020-12-21 09:39:26 +01:00
Son NK
da5bb6f9b5 no need to add alias to To header 2020-12-19 17:28:18 +01:00
Son NK
df3e594a53 update manual subscription reminder email 2020-12-19 17:23:19 +01:00
Son NK
be57add431 add list of cryptocurrency that are supported 2020-12-19 17:22:04 +01:00
Son NK
9f9d292754 remove can_use_coinbase column 2020-12-19 16:31:16 +01:00
Son NK
b3d1085e0c fix test 2020-12-18 16:52:03 +01:00
Son NK
10f15f78c8 optimize import 2020-12-18 16:25:30 +01:00
Son NK
8111beb6ff refactoring test 2020-12-18 16:24:38 +01:00
Son NK
3015cd1dc0 ignore "text/calendar" content type 2020-12-18 16:07:32 +01:00
Son NK
e48f19afb5 use info level for set message-id 2020-12-18 15:34:01 +01:00
Son NK
6cec373b6d ignore UnicodeDecodeError in decode_text() 2020-12-18 13:10:33 +01:00
Son Nguyen Kim
9886f7c327
Merge pull request #349 from simple-login/encoding-bug
Fix the encoding bug with generic subject option and Protonmail
2020-12-18 10:59:52 +01:00
Son NK
fb8a43fd5d use decode_text() in add_header() 2020-12-18 10:44:42 +01:00
Son NK
091ff3ad2c Add decode_text() 2020-12-18 10:43:06 +01:00
Son NK
efc6b32ce0 black 2020-12-16 20:34:31 +01:00
Son NK
866ef1c139 increase the max_nb_alert 2020-12-16 20:30:27 +01:00
Son NK
2f803e4714 refactoring: move template to folder 2020-12-16 18:51:14 +01:00
Son NK
ae9abe8512 remove unused var 2020-12-16 18:50:09 +01:00
Son NK
ef4ed8ca74 improve bounce email wording 2020-12-16 18:50:09 +01:00
Son NK
30b2182694 use should_disable to decide whether an alias should be disabled 2020-12-16 18:50:09 +01:00
Son NK
3a8cdce650 Create should_disable 2020-12-16 18:50:09 +01:00
Son Nguyen Kim
eb07ba8eef
Merge pull request #348 from simple-login/snyk-upgrade-8564ad7d8a9b38b2ca12605d88808620
[Snyk] Upgrade @sentry/browser from 5.27.4 to 5.27.6
2020-12-15 18:10:53 +01:00
Son NK
20094c9943 add Bing and Yandex webmaster tag 2020-12-15 18:09:50 +01:00
snyk-bot
6982a9bc5e
fix: upgrade @sentry/browser from 5.27.4 to 5.27.6
Snyk has created this PR to upgrade @sentry/browser from 5.27.4 to 5.27.6.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-12-15 03:23:47 +00:00
Son NK
f1f6234248 Add nb_manual_premium, nb_coinbase_premium to stats 2020-12-14 20:45:30 +01:00
Son Nguyen Kim
d161ca94f6
Merge pull request #347 from simple-login/coinbase
Coinbase integration
2020-12-14 11:52:50 +01:00
Son NK
830331d9b3 improve wording 2020-12-14 11:52:35 +01:00
Son NK
b9dba9c2c3 improve wording 2020-12-14 11:48:08 +01:00
Son NK
6eaeb1fcf6 update coinbase webhook 2020-12-14 11:36:34 +01:00
Son NK
3dd8ed7840 Create /dashboard/coinbase_checkout, remove extend_subscription route 2020-12-14 11:34:59 +01:00
Son NK
f19a7e1bca Add COINBASE_API_KEY and COINBASE_YEARLY_PRICE config 2020-12-14 11:33:39 +01:00
Son NK
369c9dafce remove duplicated 2020-12-13 19:56:17 +01:00
Son NK
530160567b fix coinbase button not displayed on pricing page 2020-12-13 19:51:00 +01:00
Son NK
1f4631821b fix 2020-12-13 19:41:42 +01:00
Son NK
7094a0b694 remove unused var 2020-12-13 19:31:16 +01:00
Son NK
1348b58672 only show coinbase option for user who has can_use_coinbase=True 2020-12-13 19:28:46 +01:00
Son NK
1961d2f18e Add User.can_use_coinbase column 2020-12-13 19:28:13 +01:00
Son NK
43a021dd88 send a reminder when a coinbase subscription is ending soon 2020-12-13 19:18:58 +01:00
Son NK
b00841f679 add /coinbase to handle Coinbase callback 2020-12-13 19:18:23 +01:00
Son NK
fbe48b7b3e add extend subscription link on settings page 2020-12-13 19:14:54 +01:00
Son NK
6c21b83975 add coinbase option on pricing page 2020-12-13 19:14:11 +01:00
Son NK
436e31229f Create extend_subscription page 2020-12-13 19:13:26 +01:00
Son NK
794e7ca5b9 Install coinbase-commerce 2020-12-13 19:12:02 +01:00
Son NK
0542adb761 Add COINBASE_WEBHOOK_SECRET, COINBASE_CHECKOUT_ID config 2020-12-13 19:11:49 +01:00
Son NK
02c74e6a5a take into account Coinbase in can_upgrade(), is_paid(), _lifetime_or_active_subscription() 2020-12-13 19:08:06 +01:00
Son NK
9329cf04ad Create CoinbaseSubscription model 2020-12-13 19:05:43 +01:00
Son NK
8527fed69e pricing page: improve wording 2020-12-13 17:04:05 +01:00
Son Nguyen Kim
65e001a33a
Merge pull request #345 from simple-login/email-thread
Make sure the email thread is correct when replying to a forwarded email
2020-12-11 16:39:14 +01:00
Son NK
8d72d66d08 keep References and In-Reply-To in reply phase for a correct email thread 2020-12-11 11:13:19 +01:00
Son NK
b2e1682704 do not override message-id in forward phase 2020-12-11 11:12:38 +01:00
Son NK
c1ad161db7 add email_log to get_spam_score 2020-12-11 11:05:01 +01:00
Son NK
93503d4cd3 Do not rely on revert to delete EmailLog object when pgp fails 2020-12-11 11:03:52 +01:00
Son NK
f03bde1d8d remove _MESSAGE_ID 2020-12-11 11:02:52 +01:00
Son NK
41389c7444 ignore adhoc 2020-12-08 19:03:37 +01:00
Son Nguyen Kim
bf139f83b3
Merge pull request #342 from herrboyer/linting
Linting
2020-12-07 17:45:50 +01:00
Son Nguyen Kim
982d4e692a
Merge pull request #343 from simple-login/disable-directory
Able to disable directory on-the-fly alias creation
2020-12-07 11:13:53 +01:00
Son NK
ce3dae2a07 inform user when an alias can't be created when a directory is disabled 2020-12-07 10:55:13 +01:00
Son NK
37a74bc093 refactor: rename 2020-12-07 10:50:42 +01:00
Son NK
4fb7b7bd2c user can change directory disabled 2020-12-07 10:49:40 +01:00
Son NK
05d4ec1c2f Add directory.disabled column 2020-12-07 10:48:43 +01:00
Renaud Boyer
df565bca1c fix filter_by 2020-12-07 10:38:19 +01:00
Son NK
a40bbe74fe show include_sender_in_reverse_alias as checked if user hasn't set any value 2020-12-07 10:23:36 +01:00
Son Nguyen Kim
811b33a56a
Merge pull request #341 from simple-login/revert-reverse-alias-generation
Revert reverse alias generation
2020-12-07 10:17:58 +01:00
Renaud Boyer
7838ff3224 comments for flake8 settings 2020-12-06 22:33:55 +01:00
Renaud Boyer
73a7527b5e ignore E203 2020-12-06 22:31:42 +01:00
Renaud Boyer
c61dd9dec6 linting step in workflow 2020-12-06 22:21:18 +01:00
Renaud Boyer
0e70e5cf18 flake8 hook 2020-12-06 22:18:58 +01:00
Renaud Boyer
5d948faf56 black 2020-12-06 22:11:58 +01:00
Renaud Boyer
f2f13958c7 linting 2020-12-06 22:11:22 +01:00
Renaud Boyer
a2f3aeeece linting 2020-12-06 22:10:42 +01:00
Renaud Boyer
2b9cb44cdb linting 2020-12-06 22:10:16 +01:00
Renaud Boyer
c09b6ef675 linting 2020-12-06 22:08:35 +01:00
Renaud Boyer
1c73f07d18 linting 2020-12-06 22:08:05 +01:00
Renaud Boyer
a8d67f94e2 linting 2020-12-06 22:07:18 +01:00
Renaud Boyer
f3303ee6bb linting 2020-12-06 22:06:03 +01:00
Renaud Boyer
4e93e511ec linting 2020-12-06 22:05:13 +01:00
Renaud Boyer
f1fb0ebe1f linting 2020-12-06 22:04:21 +01:00
Renaud Boyer
b7e6270a18 linting 2020-12-06 22:03:47 +01:00
Renaud Boyer
a45b6df78c linting 2020-12-06 22:03:06 +01:00
Renaud Boyer
479a9d1a35 linting 2020-12-06 22:02:18 +01:00
Renaud Boyer
47b1398cad linting 2020-12-06 22:01:43 +01:00
Renaud Boyer
4bf22771af linting 2020-12-06 22:01:38 +01:00
Renaud Boyer
43d9dbc1fc linting 2020-12-06 22:00:01 +01:00
Son NK
b0e39949cb fix embed video on mobile view 2020-12-06 19:39:12 +01:00
Son NK
fec281b84f Add include_sender_in_reverse_alias on Setting page 2020-12-06 19:38:37 +01:00
Son NK
ce2d68a64d take into account include_sender_in_reverse_alias when creating reverse-alias 2020-12-06 19:37:20 +01:00
Son NK
eab09d8c32 Add User.include_sender_in_reverse_alias column. Null for existing user, False for new user. 2020-12-06 19:36:39 +01:00
Renaud Boyer
7bac9e82b9 linting 2020-12-06 18:04:29 +01:00
Renaud Boyer
7e1a474875 linting 2020-12-06 18:02:47 +01:00
Renaud Boyer
1ced8f76b7 linting 2020-12-06 18:02:23 +01:00
Renaud Boyer
4bcc0d107f linting 2020-12-06 18:01:55 +01:00
Renaud Boyer
fad64ff064 linting 2020-12-06 18:00:41 +01:00
Renaud Boyer
fefbaeb143 linting 2020-12-06 18:00:06 +01:00
Renaud Boyer
1e5185b328 linting 2020-12-06 17:59:07 +01:00
Renaud Boyer
bcdb4c08d9 linting 2020-12-06 17:58:56 +01:00
Renaud Boyer
a46a03be85 linting 2020-12-06 17:57:10 +01:00
Renaud Boyer
0d535c8765 linting 2020-12-06 17:54:54 +01:00
Renaud Boyer
6945cb633d linting 2020-12-06 17:49:26 +01:00
Renaud Boyer
079e548ab7 linting 2020-12-06 17:48:24 +01:00
Renaud Boyer
7bcc72cc02 linting 2020-12-06 17:47:37 +01:00
Renaud Boyer
f360488eca linting 2020-12-06 17:47:05 +01:00
Renaud Boyer
0f48121fd5 linting 2020-12-06 17:46:10 +01:00
Renaud Boyer
d88aeeab7f linting 2020-12-06 17:45:07 +01:00
Renaud Boyer
6b416bcbbe linting 2020-12-06 14:51:13 +01:00
Renaud Boyer
99b4fc9625 linting 2020-12-06 14:13:20 +01:00
Renaud Boyer
57ef3ac35c linting 2020-12-06 14:10:13 +01:00
Renaud Boyer
419051cdd5 linting 2020-12-06 14:05:38 +01:00
Renaud Boyer
f746d17a02 linting 2020-12-06 14:05:00 +01:00
Renaud Boyer
b6c311a02e linting 2020-12-06 14:02:37 +01:00
Renaud Boyer
b16bfaac35 liniting 2020-12-06 13:54:59 +01:00
Renaud Boyer
ced9c879d3 linting 2020-12-06 13:48:42 +01:00
Renaud Boyer
21e928548f linting 2020-12-06 13:37:45 +01:00
Renaud Boyer
7ffe1c93f1 linting 2020-12-06 13:36:32 +01:00
Renaud Boyer
1fa64941c9 linitng 2020-12-06 11:25:41 +01:00
Renaud Boyer
20b54ca248 setup flake8 2020-12-06 11:25:32 +01:00
Renaud Boyer
5a24c7e2ae install flake8 2020-12-06 11:25:24 +01:00
Son Nguyen Kim
2d9abe0ea4
Merge pull request #338 from simple-login/snyk-upgrade-1bb610599c6398d0345af8b097c3582a
[Snyk] Upgrade @sentry/browser from 5.27.3 to 5.27.4
2020-12-05 18:27:04 +01:00
Son Nguyen Kim
82f3751350
Merge pull request #339 from herrboyer/coverage
Measure test coverage
2020-12-05 18:26:22 +01:00
Son NK
d150dfacdb rename contact_from_header -> from_header 2020-12-05 18:15:53 +01:00
Son NK
8c5f311367 prefer using Reply-To header when creating a new contact 2020-12-05 18:15:00 +01:00
Renaud Boyer
0023627bf5 store htmlcov as a workflow artefact 2020-12-05 15:03:13 +01:00
Renaud Boyer
db0114bf16 setup pytest-cov 2020-12-05 15:00:56 +01:00
Renaud Boyer
5fc1606fb5 install pytest-cov 2020-12-05 15:00:33 +01:00
snyk-bot
537617ae34
fix: upgrade @sentry/browser from 5.27.3 to 5.27.4
Snyk has created this PR to upgrade @sentry/browser from 5.27.3 to 5.27.4.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-12-05 03:23:53 +00:00
Son NK
83df119178 do not replace for "application/*" 2020-12-04 11:33:49 +01:00
Son NK
59daaa3164 ignore multipart/signed when replacing message 2020-12-03 17:52:11 +01:00
Son NK
255a7e085a add index on user_id, mailbox_id, ... if possible 2020-12-02 17:33:03 +01:00
Son NK
81f9f9f41b optimize get_stats(): use session.query instead of EmailLog.query 2020-12-02 17:30:56 +01:00
Son NK
450b101e6e add /live endpoint 2020-12-02 16:34:28 +01:00
Son NK
b7fdbe7721 log more 2020-12-02 12:40:29 +01:00
Son NK
66abbf2614 make sure to remove \n from alias name 2020-12-02 12:25:23 +01:00
Son NK
8551dade7c hide replace reverse alias option 2020-12-01 18:36:24 +01:00
Son NK
cd680bcd7f ignore content type like image/, video/, audio/ in replace() 2020-12-01 18:34:38 +01:00
Son NK
2107bd4b08 remove Black Friday promo 2020-12-01 10:07:53 +01:00
Son NK
652bb6a369 take into account multipart/mixed in replace() 2020-11-30 19:45:45 +01:00
Son NK
692c81ac2a add warning about Gmail issue if reverse-alias replacement option 2020-11-30 19:16:10 +01:00
Son NK
4d89ac4158 replace replace_str_in_msg() by replace() 2020-11-30 15:15:44 +01:00
Son NK
cefa68d392 Create replace() in email_utils 2020-11-30 15:15:13 +01:00
Son NK
22b082fd55 use warning for invalid contact email 2020-11-30 10:50:16 +01:00
Son NK
0b95ca33b8 use no encoding for 8bit and binary 2020-11-30 10:49:04 +01:00
Son NK
f804332c2d refactor: create EmailEncoding enum 2020-11-30 10:48:16 +01:00
Son NK
93563178a7 correct alias name when it contains linebreak 2020-11-27 20:37:55 +01:00
Son NK
59745b68d0 use warning level when cannot encrypt using python-gnupg 2020-11-27 20:35:57 +01:00
Son NK
86636b2eb7 fix delete user profile picture 2020-11-27 16:36:21 +01:00
Son Nguyen Kim
84e64d4c4f
Merge pull request #337 from simple-login/header-encoding
Fix the encoding issue when adding header
2020-11-26 19:08:02 +01:00
Son NK
d61f45ea86 use 7bit as default encoding 2020-11-26 17:22:17 +01:00
Son NK
1241838b26 take into account message encoding in add_header() 2020-11-26 17:03:50 +01:00
Son NK
e2a7061429 add get_encoding() and encode_text() 2020-11-26 17:01:05 +01:00
Son NK
da8b0089ff add meta name description 2020-11-26 11:50:43 +01:00
Son NK
1801fa1a4b remove environment tag in sentry action 2020-11-26 10:54:28 +01:00
Son NK
d35faf7154 comment out paddle issue annoucement 2020-11-26 10:39:44 +01:00
Son NK
e7b83fadbc trigger build 2020-11-26 10:38:48 +01:00
Son NK
083398522c sentry init with release 2020-11-26 10:30:03 +01:00
Son Nguyen Kim
83e38274e7
Merge pull request #335 from simple-login/snyk-upgrade-f062b56eb6080eaaf3e27d94885d7e0f
[Snyk] Upgrade @sentry/browser from 5.27.2 to 5.27.3
2020-11-26 10:28:02 +01:00
Son Nguyen Kim
56a74c961c
Merge pull request #336 from simple-login/test-pr
Test pr
2020-11-26 10:27:43 +01:00
Son NK
adfbfe8026 Use load_public_key_and_check when adding new PGP key 2020-11-26 10:27:32 +01:00
Son NK
5b9eb8686a add id to mailbox repr 2020-11-26 10:08:09 +01:00
Son NK
3efa96020b use warning level for invalid contact email 2020-11-26 10:06:16 +01:00
Son NK
25d7709a8b create sentry release 2020-11-26 10:04:51 +01:00
Son NK
4c1bf68d86 only push docker image on master 2020-11-26 10:04:39 +01:00
Son NK
e2f0a72ab7 log headers in case a contact email is skipped 2020-11-26 09:49:03 +01:00
snyk-bot
2b429b1738
fix: upgrade @sentry/browser from 5.27.2 to 5.27.3
Snyk has created this PR to upgrade @sentry/browser from 5.27.2 to 5.27.3.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-11-26 03:24:18 +00:00
Son NK
dcd116f11a use mail_from as fallback when contact_email is not valid 2020-11-25 17:50:25 +01:00
Son NK
03383eb181 display Paddle issue as announcement on pricing page 2020-11-25 17:43:02 +01:00
Son NK
ca625e60d5 fix add pgp 2020-11-25 17:29:54 +01:00
Son NK
ade21bc0c4 check contact email in sanity_check() 2020-11-25 15:26:17 +01:00
Son NK
ac1a6f5613 skip invalid contact in replace_header_when_forward() 2020-11-25 15:21:01 +01:00
Son NK
dfcb74dc87 do not create contact with invalid email in get_or_create_contact() 2020-11-25 15:20:42 +01:00
Son NK
609d59d23f improve logging 2020-11-25 15:20:00 +01:00
Son NK
2f882b81fe sleep before retry 2020-11-25 14:56:54 +01:00
Son NK
5e1a68cdee retry 1 more if SMTPServerDisconnected 2020-11-25 14:43:02 +01:00
Son NK
bb8c9451c4 catch all exception in load_public_key() 2020-11-25 14:31:14 +01:00
Son NK
4f211bba61 fix subject not correctly decoded in spf-fail email 2020-11-25 09:53:27 +01:00
Son Nguyen Kim
5fe48e4821
Merge pull request #333 from simple-login/email-log
Email log
2020-11-25 09:45:25 +01:00
Son NK
4381314f6f preload email_log.mailbox 2020-11-24 21:51:25 +01:00
Son NK
272c5628bb fix 2020-11-24 17:02:09 +01:00
Son NK
297857a140 Remove _MAILBOX_ID_HEADER 2020-11-24 16:50:55 +01:00
Son NK
5231483026 add doc 2020-11-24 16:38:54 +01:00
Son NK
fb465ba03e use exception log for the case where mailbox is an alias 2020-11-24 16:38:49 +01:00
Son NK
54942cdf65 set EmailLog.mailbox_id in forward and reply phase. A EmailLog for each mailbox in forward phase. 2020-11-24 16:38:34 +01:00
Son NK
6b07be5677 add email_log.mailbox_id column 2020-11-24 16:35:16 +01:00
Son NK
bcb2657de3 add remove button on custom domain name 2020-11-24 12:02:47 +01:00
Son NK
c28872288b black 2020-11-24 11:28:14 +01:00
Son NK
3d75ef974a user can turn on/off pgp on mailbox that has valid pgp_finger_print 2020-11-24 11:22:41 +01:00
Son NK
cbbb472d06 refactor 2020-11-24 11:22:41 +01:00
Son NK
c707342695 Use pgp_enabled() instead of pgp_finger_print 2020-11-24 11:22:41 +01:00
Son NK
5997e5b5b5 add Mailbox.disable_pgp column 2020-11-24 11:22:41 +01:00
Son Nguyen Kim
ab861b3624
Merge pull request #332 from simple-login/snyk-upgrade-f41978810458c6870dbb2a663d81b062
[Snyk] Upgrade @sentry/browser from 5.26.0 to 5.27.2
2020-11-24 09:32:38 +01:00
snyk-bot
5cdd3e1969
fix: upgrade @sentry/browser from 5.26.0 to 5.27.2
Snyk has created this PR to upgrade @sentry/browser from 5.26.0 to 5.27.2.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-11-24 03:24:00 +00:00
Son Nguyen Kim
a9bd313d52
Merge pull request #324 from simple-login/snyk-upgrade-bf37bdbfd90b8c29bd0e75d289390ab6
[Snyk] Upgrade @sentry/browser from 5.25.0 to 5.26.0
2020-11-23 11:08:52 +01:00
Son Nguyen Kim
c5636ece1d
Merge pull request #331 from brainynai/Readme_Correction
Readme spelling correction.
2020-11-23 11:08:38 +01:00
Son NK
33e6342a9c use warning log when user uses premium domain 2020-11-23 10:43:50 +01:00
Son NK
836b602316 fix 2020-11-22 13:50:57 +01:00
Son NK
bcdf522174 create normalize_reply_email(): handle case where reply email contains space, quote, etc 2020-11-22 13:07:09 +01:00
Son NK
a1d5b01143 make sure mailbox email is valid 2020-11-22 12:18:31 +01:00
Son NK
824a610aa6 refactor test_mailbox 2020-11-22 12:15:32 +01:00
Son NK
fbf242f6c6 handle new name can be null 2020-11-22 12:10:19 +01:00
Ian McKenzie
7dc97efb4b Readme spelling correction.
"loose" is the opposite of "tight", "lose" is the opposite of "acquire".
2020-11-21 14:51:15 -05:00
Son NK
f069d2f083 use getaddresses to parse multiple address from To, CC header. Remove get_addrs_from_header() 2020-11-21 19:15:02 +01:00
Son NK
8aed5ced3f make sure a custom domain name does not contain a linebreak 2020-11-20 18:40:07 +01:00
Son NK
78ddf16c87 make sure alias name does not contain a linebreak 2020-11-20 18:39:23 +01:00
Son NK
c25a5b50f6 make sure reply_email only contain lowercase 2020-11-20 10:03:40 +01:00
Son Nguyen Kim
d108d7b8b7
Merge pull request #330 from herrboyer/patch-1
Fix Firefox Add-on link
2020-11-20 09:50:56 +01:00
Renaud Boyer
d3ef6bc1fd
Fix Firefox Add-on link 2020-11-20 09:43:54 +01:00
Son NK
52c1adfd38 improve footer 2020-11-18 16:45:54 +01:00
Son NK
c53fe90484 fix email subject 2020-11-18 16:16:37 +01:00
Son NK
24548ff945 add reply email ascii check to sanity() 2020-11-18 16:12:00 +01:00
Son NK
30185a2798 handle the case where reply_email is not ascii 2020-11-18 16:11:00 +01:00
Son NK
75c3fa1c11 make sure reply-email only uses ascii-encoded char 2020-11-18 15:36:39 +01:00
Son NK
ed22701cbe fix duplicate pinned alias in get_alias_infos_with_pagination_v3 2020-11-18 12:08:36 +01:00
Son NK
78cb49095a fix reply_email not set 2020-11-18 11:48:09 +01:00
Son NK
9ca129cb97 use warning level for cannot decode error 2020-11-18 11:43:25 +01:00
Son NK
5b9dc88c67 make sure alias prefix cannot be more than 40 chars 2020-11-18 10:38:35 +01:00
Son NK
0224e5f8a6 Fix SpamAssassin: init all class fields 2020-11-18 10:28:32 +01:00
Son NK
319078fceb use contact email when generating reply-email 2020-11-18 10:24:39 +01:00
Son NK
e06c872bc0 add test for get_addrs_from_header() 2020-11-18 10:04:23 +01:00
Son NK
0963049d1f use utf-8 when unknown charset in parseaddr_unicode() 2020-11-18 10:03:58 +01:00
Son NK
28d42a7a22 Use get_header_unicode() in get_addrs_from_header() 2020-11-18 10:03:00 +01:00
Son NK
be510ea1d7 use utf-8 if unknown charset in get_header_unicode() 2020-11-18 10:02:10 +01:00
Son NK
391318cbaa add referred user to stats 2020-11-17 20:48:26 +01:00
Son NK
6ed6218895 black 2020-11-17 09:27:45 +01:00
Son NK
145fc9c67c Add reverse_alias_address to GET /api/aliases/:alias_id/activities 2020-11-17 09:27:30 +01:00
Son NK
aada12f17e return reverse_alias_address in POST /api/aliases/:alias_id/contacts and GET /api/aliases/:alias_id/contacts 2020-11-16 19:39:00 +01:00
Son NK
9154b4656d refactor: create is_reply_email() 2020-11-16 19:22:19 +01:00
Son NK
3d153f5203 make sure user can't choose "ra" as directory name 2020-11-16 19:18:33 +01:00
Son NK
1926408a13 Add dns_utils.get_ns() 2020-11-16 19:16:06 +01:00
Son NK
75ba1669e0 Create generate_reply_email() and refactor 2020-11-16 19:15:09 +01:00
Son NK
5781bebfd0 improve wording 2020-11-16 10:48:13 +01:00
Son NK
71c1b7cc45 add to doc 2020-11-15 19:43:46 +01:00
Son NK
15a6d9630a Handle sender_format in PATCH /api/setting 2020-11-15 19:43:37 +01:00
Son NK
c794e73abd Add EnumE.has_name(), EnumE.get_value() 2020-11-15 19:43:01 +01:00
Son NK
f0f81930bc Return "sender_format" in GET /api/setting 2020-11-15 19:35:07 +01:00
Son NK
dec7969ead add tests.utils.pretty() 2020-11-15 19:34:13 +01:00
Son NK
d8c9078708 Add EnumE.get_name() 2020-11-15 19:34:00 +01:00
Son NK
aee917a3ef Add GET /api/custom_domains/:custom_domain_id/trash 2020-11-15 19:24:54 +01:00
Son NK
de495b9afe return nb_alias in GET /api/custom_domains 2020-11-15 19:13:00 +01:00
Son NK
9d24b1b88a GET /api/custom_domains 2020-11-15 19:09:25 +01:00
Son NK
f6568aca6a Return pinned in GET /api/aliases/:alias_id, GET /api/v2/aliases 2020-11-15 19:01:11 +01:00
Son NK
f500a495b7 User can pin an alias 2020-11-15 18:46:43 +01:00
Son NK
ae05c164c9 Support pinned in PATCH /api/aliases/:alias_id 2020-11-15 18:46:27 +01:00
Son NK
6c7018dd33 Take into account pinned alias in get_alias_infos_with_pagination_v3 2020-11-15 18:42:29 +01:00
Son NK
abf50e302b Add Alias.pinned column 2020-11-15 18:38:07 +01:00
Son NK
08902bf784 small style refactor 2020-11-15 18:31:10 +01:00
Son NK
9cfb6d412a Add /api/v5/alias/options 2020-11-14 16:45:22 +01:00
Son NK
f452c79aec Add /v2/setting/domains 2020-11-14 16:37:36 +01:00
Son NK
ae64bd26b9 add TOC for api.md 2020-11-14 16:32:19 +01:00
Son NK
fde01af5b5 improve readme 2020-11-14 16:26:15 +01:00
Son Nguyen Kim
521ab4f47e
Update README.md 2020-11-14 16:06:42 +01:00
Son NK
56aca5edaf use another hero image 2020-11-14 16:03:25 +01:00
Son NK
81e211f8b4 Move table of content to top 2020-11-14 16:03:16 +01:00
Son NK
ea6e6f23d2 Move api and oauth to dedicated page 2020-11-14 16:00:39 +01:00
Son NK
496be08639 handle case where contact address is empty/invalid 2020-11-14 15:55:53 +01:00
Son NK
f62c568dd0 Add Contact.invalid_email column 2020-11-14 15:54:06 +01:00
Son NK
3489e41fdb Add NOREPLY setting 2020-11-14 15:53:20 +01:00
Son NK
2c46d1db8e Add PGP_SIGNER and display if it's set 2020-11-14 13:00:14 +01:00
Son NK
a072d6c0cd only report the paddle error if it's not 147 2020-11-14 12:55:53 +01:00
Son NK
f16676e921 email.message_from_string can also throw LookupError when non-existent charset is set 2020-11-13 18:08:56 +01:00
Son NK
b4e5e3eecb user can choose a referral code 2020-11-13 16:18:09 +01:00
Son NK
25de8001e2 fix circular import 2020-11-12 17:56:03 +01:00
Son NK
1c061ceb59 Schedule domain deletion instead of deleting it immediately 2020-11-12 17:41:47 +01:00
Son NK
b4d1b3950d log SA report 2020-11-12 12:11:39 +01:00
Son NK
ec4d879836 change promotion to 1st year only 2020-11-10 22:37:58 +01:00
Son NK
5570300699 use 2 random words when creating random alias 2020-11-10 18:52:31 +01:00
Son NK
4e16eb7403 fix 2020-11-10 17:42:23 +01:00
Son NK
4666d21f63 email.message_from_string can also throw KeyError when 'content-transfer-encoding' is absent 2020-11-10 17:26:24 +01:00
Son NK
632a5bbbc8 handle UnicodeEncodeError in copy() and to_bytes() 2020-11-10 16:02:19 +01:00
Son NK
c3f73b25b2 decode the subject 2020-11-09 21:16:50 +01:00
Son NK
b2f9479bce failover when ascii encoding fails 2020-11-09 20:58:39 +01:00
Son NK
44c3ac1741 Replace 2.99 by 3, 29.99 by 30 2020-11-09 17:56:26 +01:00
Son NK
a35256d161 fix paddle 2020-11-09 17:34:58 +01:00
Son NK
21839d579c log more 2020-11-09 17:03:47 +01:00
Son NK
2cc7cb6a37 use to_bytes instead of .as_bytes() 2020-11-09 17:02:10 +01:00
Son NK
15466903d1 Support OTHER_PADDLE_MONTHLY_PRODUCT_IDS, OTHER_PADDLE_YEARLY_PRODUCT_IDS config 2020-11-09 16:56:17 +01:00
Son NK
3f8e5d0a8b add more logging 2020-11-08 22:39:49 +01:00
Son NK
a9297078d3 fix dkim clipboard 2020-11-08 22:39:38 +01:00
Son NK
76389647bb add black friday coupon code 2020-11-08 16:31:24 +01:00
Son NK
4d03d2fe04 Fix subject 2020-11-07 17:23:28 +01:00
Son NK
78f5f27d5d add more debugging 2020-11-07 16:12:28 +01:00
Son Nguyen Kim
44fd80b2e1
Merge pull request #327 from simple-login/generic-subject
Generic subject for PGP-encrypted forwarded emails
2020-11-07 13:02:45 +01:00
Son NK
4be182320e black 2020-11-07 13:00:58 +01:00
Son NK
6a68141d8d Use mailbox generic subject for forwarded emails 2020-11-07 13:00:45 +01:00
Son NK
606f9dfbae use valid PGP key for fake data 2020-11-07 13:00:26 +01:00
Son NK
e659680875 add_header() 2020-11-07 13:00:12 +01:00
Son NK
f57f29a97b Able to set a generic subject for PGP-enabled mailbox 2020-11-07 12:58:51 +01:00
Son NK
7cc57106de Add Mailbox.generic_subject column 2020-11-07 12:48:44 +01:00
Son NK
8b356eef01 remove a test that can randomly fail 2020-11-05 10:48:32 +01:00
Son NK
5ef3ab4d74 disable handle_bounce_reply_phase 2020-11-05 10:26:19 +01:00
Son NK
d0ca773376 not forward email that has invalid from address 2020-11-05 10:26:09 +01:00
Son NK
352cd978bd add debug 2020-11-04 19:42:20 +01:00
Son NK
6585aef443 use warning level 2020-11-04 16:11:32 +01:00
Son NK
3dee121bec improve handle_bounce_reply_phase 2020-11-04 15:38:26 +01:00
Son NK
3a03dec077 simplify code 2020-11-04 14:55:54 +01:00
Son NK
009236e623 add debug code when bounce message cannot be parsed 2020-11-04 13:37:33 +01:00
Son NK
6c626520d3 handle_bounce_reply_phase 2020-11-04 12:32:15 +01:00
Son NK
f4f2db0f04 use the same error structure in apple endpoints 2020-11-03 16:13:10 +01:00
Son NK
6e0394d980 fix filter not working when alias has several mailboxes 2020-11-03 15:10:57 +01:00
Son NK
5a7df14d58 use msg.as_bytes() to sign instead of as_string() 2020-11-03 13:30:37 +01:00
Son NK
4b8a2a1851 add tests for sign_data 2020-11-03 13:30:13 +01:00
Son NK
c1b8f717b5 improve DNS page 2020-11-03 13:12:22 +01:00
Son NK
4b6368b378 make sure only verified mailbox can be used as default 2020-11-03 12:43:01 +01:00
Son NK
19ac657c1c add GET /api/v2/mailboxes 2020-11-03 12:14:13 +01:00
Son NK
5ee74c74b6 do not use url_for() in api tests 2020-11-03 12:07:48 +01:00
Son NK
273537e7ae use same mailbox format for "POST /api/mailboxes" and "GET /api/mailboxes" 2020-11-03 11:22:01 +01:00
Son NK
a989545505 make sure alias contact address is valid 2020-11-03 11:13:43 +01:00
Son NK
04a418e655 fix contact name always converted to lowercase 2020-11-03 11:11:55 +01:00
Son NK
9fafddd603 use is_valid_email instead of regex 2020-11-03 11:11:09 +01:00
Son NK
751cc05534 check contact address in POST /aliases/<int:alias_id>/contacts 2020-11-03 11:10:32 +01:00
Son NK
72a34e28be add is_valid_email() 2020-11-03 11:09:37 +01:00
Son NK
fe6e9fa435 install py3-validate-email 2020-11-03 10:50:47 +01:00
Son NK
a890557c7f Use check_alias_prefix() to check alias prefix 2020-11-03 10:39:08 +01:00
snyk-bot
42237c9539
fix: upgrade @sentry/browser from 5.25.0 to 5.26.0
Snyk has created this PR to upgrade @sentry/browser from 5.25.0 to 5.26.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-11-03 03:23:45 +00:00
Son NK
8d0e243c83 sign PGP forwarded email if PGP_SENDER_PRIVATE_KEY 2020-11-02 19:09:57 +01:00
Son NK
3f150e5944 add new param PGP_SENDER_PRIVATE_KEY_PATH 2020-11-02 19:06:47 +01:00
Son NK
63788125da save bounce email sent to an alias when 2020-11-02 15:10:03 +01:00
Son NK
c41c36acaa set "date" header in forward phase if needed 2020-11-02 14:53:22 +01:00
Son NK
38877598cf fix handle_reply: do not delete _MIME_HEADERS headers, add Date header 2020-11-02 14:51:37 +01:00
Son Nguyen Kim
d2f2053738
Merge pull request #321 from simple-login/snyk-upgrade-ded98394f6af903837d29c0e8b52b575
[Snyk] Upgrade bootbox from 5.4.0 to 5.5.1
2020-11-01 20:56:40 +01:00
Son NK
756e8080ab handle case msg.get_payload(decode=True) is None 2020-11-01 18:38:21 +01:00
Son NK
1d0aa0f900 fix doc 2020-11-01 18:13:50 +01:00
Son NK
7337110110 Remove all headers in reply phase 2020-11-01 18:12:09 +01:00
Son NK
593e81705b Handle case Content-Type and Mime-Version are missing in prepare_pgp_message 2020-11-01 18:06:28 +01:00
Son NK
53e57eee42 clone orig message in prepare_pgp_message 2020-11-01 18:06:05 +01:00
Son NK
7ca74eaa6f replace-reverse-alias and pgp encryption before modifying message header in reply phase 2020-11-01 18:02:43 +01:00
Son NK
ec1b7dd8b8 return latest_activity=null if there's no activity in GET /api/v2/aliases 2020-11-01 12:32:20 +01:00
Son NK
92ea8de374 fix test name 2020-11-01 12:29:15 +01:00
Son NK
38ca2341bc fix mailbox query in get_alias_infos_with_pagination_v3 2020-11-01 12:24:19 +01:00
Son NK
e49169b887 Reset default random alias domain setting if user is not premium 2020-11-01 09:37:09 +01:00
Son NK
fef6edf619 use warning for domain DNS fails 2020-11-01 09:32:41 +01:00
snyk-bot
6df616c4b4
fix: upgrade bootbox from 5.4.0 to 5.5.1
Snyk has created this PR to upgrade bootbox from 5.4.0 to 5.5.1.

See this package in npm:
https://www.npmjs.com/package/bootbox

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-11-01 03:25:19 +00:00
Son NK
28563b9653 fix duplicated display 2020-10-30 13:05:45 +01:00
Son NK
c076c7c7f3 PATCH /api/setting 2020-10-28 17:47:26 +01:00
Son NK
12f7485cb1 Add GET /api/setting/domains 2020-10-28 17:30:57 +01:00
Son NK
77bf9537d0 Add GET /api/setting 2020-10-28 17:23:58 +01:00
Son NK
91534d3cf2 Add PATCH /api/user_info 2020-10-28 17:12:21 +01:00
Son NK
3f40e3c1cf return profile_picture_url in GET /user_info 2020-10-28 17:11:33 +01:00
Son NK
0307793666 use pgpy as fallback for gpg 2020-10-28 17:07:53 +01:00
Son NK
6c816d51d6 fix load_public_key_and_check, remove IncorrectPassphrasePGPException 2020-10-28 12:21:42 +01:00
Son NK
5a190ed840 use pgpy if python-gnupg fails 2020-10-28 12:21:24 +01:00
Son NK
9210459a72 add pgpy
- add pgpy to poetry
- add test PGP keys to local_data
- add encrypt_file_with_pgpy()
- use randomly pgpy
2020-10-28 11:50:14 +01:00
Son NK
71be3b27f7 redirect user to dashboard when setup is done 2020-10-28 10:49:50 +01:00
Son NK
a2254cfdf8 remove pgp debugging code 2020-10-27 20:27:34 +01:00
Son NK
1abebe8067 improve how to use reverse-alias 2020-10-27 18:59:59 +01:00
Son NK
aa1cac521b Set X-SimpleLogin-Envelope-From header in forward phase 2020-10-27 11:03:56 +01:00
Son NK
8f6550f992 update contact.mail_from and contact.from_header if needed 2020-10-27 10:40:54 +01:00
Son Nguyen Kim
5681f061b5
Merge pull request #318 from simple-login/snyk-upgrade-6b9d3fe637a4e3f1a1817e388bb7b454
[Snyk] Upgrade @sentry/browser from 5.24.2 to 5.25.0
2020-10-27 09:02:23 +01:00
snyk-bot
a8d4ef73a2
fix: upgrade @sentry/browser from 5.24.2 to 5.25.0
Snyk has created this PR to upgrade @sentry/browser from 5.24.2 to 5.25.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-10-27 03:23:50 +00:00
Son NK
dec956c84d handle case user is None 2020-10-26 12:01:59 +01:00
Son NK
4a7b73a218 use warning log for disabled account 2020-10-26 10:33:53 +01:00
Son NK
6803d4bf42 fix import error 2020-10-26 10:32:56 +01:00
Son NK
b09bb42b2d handle case where highlight_alias_id is not a number 2020-10-26 10:31:38 +01:00
Son NK
f0b46c1887 use warning log for /alias/options 2020-10-26 10:31:19 +01:00
Son NK
477481c41e save the data for debugging when pgp fails 2020-10-24 19:03:19 +02:00
Son NK
83f3309149 use warning log for /v3/alias/options 2020-10-24 16:28:07 +02:00
Son NK
c140d3f842 improve reverse-alias instruction use 2020-10-24 16:23:47 +02:00
Son NK
9f50ab4cce Handle IntegrityError when creating new alias 2020-10-24 15:50:29 +02:00
Son NK
bdec727cd1 allow mailbox's authorized address to unsubscribe alias 2020-10-23 13:29:20 +02:00
Son NK
5da7953a64 handle the case a directory alias is created concurrently 2020-10-23 11:55:01 +02:00
Son NK
bfa59dcdd9 fix 2020-10-22 12:26:45 +02:00
Son NK
da7c07fc42 use new mailbox illustration 2020-10-22 11:46:41 +02:00
Son NK
85a1d67c6f put browser extension onboarding email to the welcome email 2020-10-22 11:46:15 +02:00
Son NK
bc1eeb4f01 move TOTP to top 2020-10-22 11:09:37 +02:00
Son NK
677f150fef add unsubscribe header to com emails 2020-10-22 10:44:05 +02:00
Son NK
ea45ac119e Refactor: create Alias.unsubscribe_link 2020-10-22 10:37:02 +02:00
Son NK
f624085aa3 handle newsletter unsubscribe when the subject=user_id* 2020-10-22 10:34:52 +02:00
Son NK
4f7b30c204 Set user.paid_lifetime if paid coupon 2020-10-21 19:31:25 +02:00
Son NK
050c2feaeb Add LifetimeCoupon.paid column 2020-10-21 19:31:07 +02:00
Son NK
459f821036 fix name 2020-10-20 20:09:44 +02:00
Son NK
a001132497 improve wording 2020-10-20 18:07:21 +02:00
Son NK
14b86749df add more precision on what is account email 2020-10-20 18:02:52 +02:00
Son NK
6921ab05fd reorganise settings sections 2020-10-20 17:47:58 +02:00
Son NK
635182e1ef move lifetime case to first 2020-10-20 17:45:06 +02:00
Son NK
acce32fcc8 add doc 2020-10-20 17:42:05 +02:00
Son NK
c6b6083c46 Revert "remove alias options v1,v2,v3 tests"
This reverts commit 8da14ca8ca.
2020-10-20 17:32:01 +02:00
Son NK
32e25f5378 bring back alias/options v1,2,3 2020-10-20 17:31:43 +02:00
Son NK
8da14ca8ca remove alias options v1,v2,v3 tests 2020-10-20 17:03:32 +02:00
Son NK
44b544d768 only send custom domain alert if fails more than 5 consecutive days 2020-10-20 16:51:25 +02:00
Son NK
08e2c1b05a use a different port in new_migration.sh 2020-10-20 16:50:13 +02:00
Son NK
828799010b Add CustomDomain.nb_failed_checks column 2020-10-20 16:50:01 +02:00
Son NK
8482a55df6 display whether a domain is premium 2020-10-20 16:44:22 +02:00
Son NK
03521b5a84 do not check alias_domain_prefix when DISABLE_ALIAS_SUFFIX is set 2020-10-20 16:42:05 +02:00
Son NK
886d3a761c delete /alias/options v1,v2,v3 2020-10-19 12:08:47 +02:00
Son NK
60b1145670 improve logging msg 2020-10-16 20:26:13 +02:00
Son Nguyen Kim
ac07c775e4
Merge pull request #311 from simple-login/premium-domain
Add support for premium domain
2020-10-15 17:04:43 +02:00
Son NK
a2a4e50f27 remove potential duplicate in available_alias_domains 2020-10-15 17:02:54 +02:00
Son NK
1524bb4e4b black 2020-10-15 16:52:55 +02:00
Son NK
dbf0404aa9 rename public_domain -> sl_domain if applicable 2020-10-15 16:52:38 +02:00
Son NK
4a32db5b5d rename PublicDomain -> SLDomain 2020-10-15 16:51:07 +02:00
Son NK
0a4fc76b61 optimize import 2020-10-15 16:45:28 +02:00
Son NK
adff510359 use PublicDomain instead if ALIAS_DOMAINS 2020-10-15 16:45:08 +02:00
Son NK
521d8e51a5 small fix 2020-10-15 16:25:56 +02:00
Son NK
1fcf166c00 small refactor: add should_add_dkim_signature 2020-10-15 16:24:04 +02:00
Son NK
e79522b638 take into account Premium domains 2020-10-15 16:21:31 +02:00
Son NK
dcbd7baabc Add PublicDomain.premium_only column 2020-10-15 16:08:06 +02:00
Son NK
90163220cf rename email_domain_can_be_used_as_mailbox -> email_can_be_used_as_mailbox 2020-10-15 16:05:47 +02:00
Son NK
f56b0cddb2 use exception log for /alias/options endpoints 2020-10-15 16:02:04 +02:00
Son NK
cd15c64731 Add PREMIUM_ALIAS_DOMAINS config 2020-10-15 16:01:44 +02:00
Son NK
805e78cad1 rename email_belongs_to_alias_domains -> email_belongs_to_default_domains 2020-10-14 18:46:05 +02:00
Son NK
2d9abe55a4 use log warning for mailbox issue 2020-10-14 09:32:44 +02:00
Son Nguyen Kim
ad90a0c93e
Merge pull request #308 from simple-login/snyk-upgrade-fa685078eae6ad4fc4bbd10b4ea4b7c7
[Snyk] Upgrade @sentry/browser from 5.23.0 to 5.24.2
2020-10-13 10:12:30 +02:00
Son NK
6557b7157f handle the Paddle 147 error 2020-10-12 17:37:04 +02:00
Son NK
8268568f08 add mailbox.disabled column. Disable a mailbox if it fails tests for 10 days consecutive. 2020-10-12 13:28:21 +02:00
Son NK
987d25263c regenerate migration file 2020-10-11 20:37:52 +02:00
Son NK
7c0b3b290b only check incoming queue (ignore active queue) 2020-10-11 18:13:17 +02:00
Son NK
c2e03854ef refactor monitoring 2020-10-11 18:11:49 +02:00
Son Nguyen Kim
4e45a619cd
Merge pull request #302 from TheLastProject/feature/custom_domain_random_suffix
Support random suffix for personal domains
2020-10-11 18:06:06 +02:00
snyk-bot
23d7a83f16
fix: upgrade @sentry/browser from 5.23.0 to 5.24.2
Snyk has created this PR to upgrade @sentry/browser from 5.23.0 to 5.24.2.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-10-10 03:23:42 +00:00
Sylvia van Os
9702037573 Reformat with Black 2020-10-09 23:00:10 +02:00
Sylvia van Os
26d0437009 Make prefix generation configurable per domain 2020-10-09 22:54:13 +02:00
Son NK
5486f54955 refactor verify_prefix 2020-10-09 22:08:29 +02:00
Son NK
0f349388ca add nb_cancelled_premium to stats 2020-10-09 11:56:03 +02:00
Son NK
8dbd3c1c68 more detailed error message when an alias can't be created 2020-10-09 11:48:52 +02:00
Son NK
859bc7976e create create-alias tests when alias is already existed 2020-10-09 11:37:56 +02:00
Sylvia van Os
6b085960cb Merge branch 'master' of https://github.com/simple-login/app into feature/custom_domain_random_suffix 2020-10-07 18:52:54 +02:00
Sylvia van Os
739fb50b04 Format with Black 2020-10-05 21:12:13 +02:00
Sylvia van Os
aeef9ccca9 Support random suffix for personal domains 2020-10-05 21:00:52 +02:00
Son NK
2d73d52127 log disabled user out immediately 2020-10-04 19:17:19 +02:00
Son Nguyen Kim
a6ce047f32
Merge pull request #295 from simple-login/snyk-upgrade-fb1fd92f3691b12ba149deef1532996b
[Snyk] Upgrade @sentry/browser from 5.22.3 to 5.23.0
2020-10-04 15:08:49 +02:00
Son Nguyen Kim
a4bcf59bfe
Merge pull request #297 from TheLastProject/feature/dedupe_email
Dedupe email address
2020-10-04 15:08:15 +02:00
Son NK
6993721ae2 disable email forwards/sending if user is disabled 2020-10-04 12:49:43 +02:00
Son NK
7e425c0338 disable login if user is disabled 2020-10-04 12:49:43 +02:00
Son NK
6c37a91c6d add User.disabled field 2020-10-04 12:49:43 +02:00
Son NK
e225bffc30 add backref for some models 2020-10-04 12:49:43 +02:00
Son Nguyen Kim
9d0a896e1c
Merge pull request #299 from pojhm91c7iwk/patch-1
Update README.md (Self-hosted instructions)
2020-10-03 10:59:38 +02:00
Son Nguyen Kim
73e90e6892
Merge pull request #298 from TheLastProject/feature/autogenerated_icon
Show icon if address was autogenerated
2020-10-03 10:54:41 +02:00
Son NK
731b8db5cb monitoring: alert when fails for 10 minutes 2020-10-03 10:34:07 +02:00
Sylvia van Os
ff0eaa4bbf Show icon if address was autogenerated 2020-10-02 20:52:29 +02:00
pojhm91c7iwk
b90705f12e
Update README.md
Added instructions to update 'myuser' and 'mypassword' where appropriate.
2020-10-02 11:39:30 -07:00
Sylvia van Os
7be674c13b Reformat with Black 2020-10-01 21:51:50 +02:00
Sylvia van Os
d8ed1cbbc3 Dedupe email address 2020-10-01 21:35:11 +02:00
Son NK
b99085419e remove AioHttpIntegration sentry, remove aiocontextvars dependency 2020-10-01 12:48:08 +02:00
Son NK
b690e903fa small refactoring 2020-10-01 12:24:37 +02:00
Son NK
52b5526261 add email_validator as dependency 2020-10-01 12:21:37 +02:00
Son NK
73f56818fb fix ProxyFix 2020-10-01 12:21:16 +02:00
Son NK
351adc57f5 use poetry instead of pip 2020-10-01 12:13:28 +02:00
Son NK
9aa460d47f upgrade ruamel.yaml to avoid installation issue 2020-10-01 11:25:36 +02:00
Son NK
5985c7f655 upgrade yacron 2020-10-01 11:25:19 +02:00
snyk-bot
8b63d302e4
fix: upgrade @sentry/browser from 5.22.3 to 5.23.0
Snyk has created this PR to upgrade @sentry/browser from 5.22.3 to 5.23.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-10-01 03:23:45 +00:00
Son NK
cbdcab7d24 handle the ValueError in SpamAssassin 2020-09-30 17:24:03 +02:00
Son NK
6253a4eb23 set SpamAssassin timeout to 300s 2020-09-30 14:03:19 +02:00
Son NK
770b15aba3 do not hardcode spamd user 2020-09-30 14:01:49 +02:00
Son NK
6a4622fca9 replace cx42 by spamd for SpamAssassin
get rid of "info: spamd: handle_user (userdir) unable to find user: 'cx42'" error
2020-09-30 12:47:39 +02:00
Son NK
772a2e7355 add "export alias" button 2020-09-30 12:20:18 +02:00
Son NK
8ed619687f ignore UnicodeDecodeError when parsing SpamAssassin response 2020-09-30 12:00:05 +02:00
Son NK
c8e92af4d3 improve onboarding emails wordings 2020-09-30 11:53:07 +02:00
Son NK
8517e7d356 refactor com emails: remove non-uses, move to the right location 2020-09-30 11:51:43 +02:00
Son Nguyen Kim
38cc2e7986
Merge pull request #294 from simple-login/snyk-upgrade-34b4e08dc7744ce0d38308d7e07a263b
[Snyk] Upgrade @sentry/browser from 5.21.4 to 5.22.3
2020-09-30 11:09:34 +02:00
Son Nguyen Kim
4e7aec7dce
Merge pull request #293 from TheLastProject/patch-1
Add dot after DKIM
2020-09-30 11:09:01 +02:00
Son NK
abc42df0fb create get_spam_score() as a sync function, use a simpler version for running MailHandler. Remove async/await 2020-09-30 11:05:21 +02:00
Son NK
91e3cc5dcb able to set a different host than 127.0.0.1 and apply black format 2020-09-30 10:32:21 +02:00
Son NK
078368362c copy spamassassin client code from https://github.com/petermat/spamassassin_client 2020-09-30 10:29:52 +02:00
Son NK
f2eedfd3d1 enable sentry AioHttpIntegration 2020-09-30 10:22:10 +02:00
Son NK
19c61fa656 upgrade sentry-sdk to 0.18.0 2020-09-30 10:19:22 +02:00
snyk-bot
89c91f3843
fix: upgrade @sentry/browser from 5.21.4 to 5.22.3
Snyk has created this PR to upgrade @sentry/browser from 5.21.4 to 5.22.3.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-09-30 03:23:40 +00:00
Sylvia van Os
b95c44e3db
Add dot after DKIM
To prevent the domain name being added after it
2020-09-29 19:17:56 +02:00
Son NK
61e4455406 logging more for spamassassin 2020-09-29 16:00:53 +02:00
Son NK
cc8b3a116b improve email wording 2020-09-29 13:37:47 +02:00
Son NK
d7ca639dc1 format 2020-09-29 13:11:04 +02:00
Son NK
65938d2fb7 improve email template 2020-09-29 13:03:15 +02:00
Son NK
e8ccbced59 refactor code: wrap smtp.sendmail into sl_sendmail() 2020-09-29 12:57:14 +02:00
Son NK
e661f90ce7 do not hard exit when memory exceeds the threshold 2020-09-29 12:33:36 +02:00
Son Nguyen Kim
3a765ffc83
Merge pull request #285 from simple-login/snyk-upgrade-9ddbd7079151e5c2f8ba0b75c00326c7
[Snyk] Upgrade @sentry/browser from 5.21.3 to 5.21.4
2020-09-29 12:30:50 +02:00
Son NK
17ef292779 update reply-must-use-personal-email template 2020-09-29 11:00:50 +02:00
Son NK
571e39bb30 user can add/remove authorized address 2020-09-28 21:09:20 +02:00
Son NK
8b344e7dfe migration file 2020-09-28 17:43:16 +02:00
Son NK
0a7643b367 take into account mailbox authorized address when check for spoofing 2020-09-28 17:43:09 +02:00
Son NK
063885ccf7 Add get_mailbox_from_mail_from() 2020-09-28 17:41:16 +02:00
Son NK
0830bba218 Add AuthorizedAddress model 2020-09-28 17:40:54 +02:00
Son NK
d6d686c4c3 add argument commit= to create() 2020-09-28 17:40:30 +02:00
Son NK
bb6a5bf0b3 take into account a mailbox can be deleted in the meantime 2020-09-25 10:06:50 +02:00
Son NK
53f66d0f3c handle the "past due" subscription case: downgrade a subscription if it's renewed 2020-09-24 09:34:35 +02:00
Son NK
f274bac053 fix typo 2020-09-23 19:36:54 +02:00
Son NK
740d31871d remove asyncio.shield 2020-09-17 17:03:20 +02:00
Son NK
d81ad2fd12 sanitize mailbox before creating: remove empty space 2020-09-17 17:02:50 +02:00
Son NK
5f8fff5af3 wrap shield around aiospamc.check to avoid the task being canceled 2020-09-16 19:47:12 +02:00
Son NK
4b697fc897 parseaddr_unicode: handle case where name is bytes 2020-09-16 17:34:33 +02:00
Son NK
25118dff9b use parseaddr_unicode instead of parseaddr 2020-09-16 17:28:15 +02:00
Son NK
03dfafe1cf handle linebreak in parseaddr_unicode 2020-09-16 17:28:01 +02:00
Son NK
5c8d31111c add contact to handle_unknown_mailbox logging 2020-09-16 17:24:42 +02:00
snyk-bot
b8099bdb2f
fix: upgrade @sentry/browser from 5.21.3 to 5.21.4
Snyk has created this PR to upgrade @sentry/browser from 5.21.3 to 5.21.4.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-09-15 03:23:43 +00:00
Son NK
b9b442294b remove unnecessary sanitize on mailbox email 2020-09-14 20:02:46 +02:00
Son NK
5480f6d35b handle case highlight_id is not int 2020-09-14 19:54:00 +02:00
Son NK
a37f7fe8b8 sql migration 2020-09-14 18:22:35 +02:00
Son NK
cb2033443c fill up contact mail_from, from_header if possible 2020-09-14 18:22:26 +02:00
Son NK
8faae3d0d4 add sanitize check for alias, mailbox, contact reply-email 2020-09-14 18:22:08 +02:00
Son NK
40892f8253 contact email can contain whitespace 2020-09-14 18:21:30 +02:00
Son NK
299f7d3fba remove unnecessary sanitize 2020-09-14 17:58:15 +02:00
Son NK
491f4de120 add Contact mail_from and from_header column 2020-09-14 17:55:55 +02:00
Son NK
1ab36bd22b remove unnecessary email address sanitize 2020-09-14 17:38:48 +02:00
Son NK
ed2e748d1e sanitize envelope mail_from and rcpt_tos 2020-09-14 17:30:01 +02:00
Son NK
c48b5038f3 sanitize rcpt_to in greylisting_needed 2020-09-14 12:20:16 +02:00
Son NK
18263c2fd5 handle case alias already created in try_auto_create_catch_all_domain() 2020-09-14 12:18:15 +02:00
Son NK
e291a71037 fix duplicate contact on contact page 2020-09-14 11:38:01 +02:00
Son NK
5cfeb4c3f2 Add delta to stats 2020-09-12 16:22:48 +02:00
Son NK
85beb774c7 fix onboarding email 2020-09-12 15:51:43 +02:00
Son NK
1026f0763d fix wording 2020-09-12 14:57:33 +02:00
Son Nguyen Kim
3e450c5ac2
Merge pull request #283 from simple-login/snyk-upgrade-cf909261df1f4c89d4cc698a2c9edcd2
[Snyk] Upgrade @sentry/browser from 5.21.1 to 5.21.3
2020-09-12 14:45:20 +02:00
Son Nguyen Kim
1f55bc73d3
Merge pull request #282 from simple-login/snyk-upgrade-16f5866ca3771ef57481c5d739b22f95
[Snyk] Upgrade vue from 2.6.11 to 2.6.12
2020-09-12 14:44:57 +02:00
Son NK
fead5efc8b migration script 2020-09-12 14:34:38 +02:00
Son NK
c6eba9f125 improve welcome email 2020-09-12 14:34:32 +02:00
Son NK
09380915fb use user first alias to for onboarding emails 2020-09-12 14:33:27 +02:00
Son NK
3545ae7d97 improve intro 2020-09-12 14:32:55 +02:00
Son NK
2cfaa93a5f create a first alias to receive SimpleLogin newsletter when user is created 2020-09-12 14:31:31 +02:00
Son NK
8fe508c5d3 Add User.newsletter_alias_id column 2020-09-12 14:30:49 +02:00
Son NK
e519a917d2 refactor welcome email 2020-09-12 12:13:35 +02:00
Son NK
2fba4c9a53 send email to ask for user feedback when user cancels 2020-09-12 11:12:34 +02:00
snyk-bot
54ace01d86
fix: upgrade @sentry/browser from 5.21.1 to 5.21.3
Snyk has created this PR to upgrade @sentry/browser from 5.21.1 to 5.21.3.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-09-12 03:23:44 +00:00
Son NK
7cc90ad194 send email to ask for user feedback when user cancels 2020-09-11 18:39:57 +02:00
Son NK
c625a178e8 use RequestException instead of ConnectionError 2020-09-11 16:55:32 +02:00
Son NK
d14f6cf7fb Ignore wrong formatted row 2020-09-11 16:51:04 +02:00
Son NK
76175dc517 update batch import wording 2020-09-11 16:49:39 +02:00
Son NK
530db2fdd4 fix shell 2020-09-11 16:44:24 +02:00
snyk-bot
8f04ae82e6
fix: upgrade vue from 2.6.11 to 2.6.12
Snyk has created this PR to upgrade vue from 2.6.11 to 2.6.12.

See this package in npm:
https://www.npmjs.com/package/vue

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-09-11 03:23:48 +00:00
Son Nguyen Kim
43babcf2d9
Merge pull request #281 from simple-login/batch-import
User can batch import aliases
2020-09-10 20:20:02 +02:00
Son NK
b92966b2c6 sql migration 2020-09-10 20:15:21 +02:00
Son NK
f664243e42 add batch-import page 2020-09-10 20:14:55 +02:00
Son NK
6da48298a6 Add BatchImport model 2020-09-10 20:05:25 +02:00
Son NK
f224d16c56 mark a mailbox as unverified if it fails checks for too many times 2020-09-10 09:40:27 +02:00
Son NK
e6dd2f1717 do not forward to unverified mailbox 2020-09-10 09:38:30 +02:00
Son NK
a660a05f83 use warning for problem with random alias default domain 2020-09-10 09:32:51 +02:00
Son NK
bde6f661e4 change onboarding email order 2020-09-09 22:16:10 +02:00
Son NK
91c4b68ca3 add send_onboarding_emails() to shell 2020-09-09 22:14:41 +02:00
Son NK
85332a5fb5 improve onboarding email subjects 2020-09-09 22:14:06 +02:00
Son NK
ab5cd37f70 improve send-from-alias onboarding email 2020-09-09 22:02:56 +02:00
Son NK
a46a1dfaea add raw url for pgp onboarding email 2020-09-09 20:38:23 +02:00
Son NK
e3e9428247 improve onboarding mailbox email 2020-09-09 20:38:05 +02:00
Son NK
32373b6bd0 improve browser-extension onboarding email 2020-09-09 20:30:58 +02:00
Son NK
b9bd167ff6 improve onboarding pgp email 2020-09-09 20:26:32 +02:00
Son NK
0c9106717b handle case where contact is concurrently created 2020-09-09 17:00:07 +02:00
Son NK
6a8c0d6f76 monitoring alert when more than 50 emails in queue. Check every 2 mins. 2020-09-08 18:52:36 +02:00
Son NK
290428009a change item order in menu 2020-09-08 13:57:13 +02:00
Son NK
b65534a8e7 move "api keys" page to dropdown menu 2020-09-08 13:56:38 +02:00
Son NK
a0b50762ee remove "how to use" on custom domain page 2020-09-08 13:51:46 +02:00
Son NK
7b6e58ef95 always show the "how to send email" help 2020-09-08 13:51:00 +02:00
Son NK
15d7f6407e fix compatible with mailvelope add name=encrypted.asc 2020-09-08 11:10:22 +02:00
Son NK
10205e3731 add pre-commit, upgrade pip-tools 2020-09-08 11:05:41 +02:00
Son NK
d1eb1ea799 handle case apple server not accessible 2020-09-05 20:56:03 +02:00
Son NK
5cf0a4bcfe handle case alias can be None in toggle_alias 2020-09-05 20:54:08 +02:00
Son NK
e36768824f handle the case contact already added 2020-09-03 19:42:52 +02:00
Son NK
b83c513607 use warning level 2020-09-03 15:43:33 +02:00
Son NK
0dbc755790 use warning level when not able to parse email-log-id 2020-09-03 15:43:01 +02:00
Son Nguyen Kim
f27a448d1b
Merge pull request #277 from simple-login/snyk-upgrade-6a61cde518cf1f5435c396f3a626eea2
[Snyk] Upgrade @sentry/browser from 5.20.1 to 5.21.1
2020-09-03 09:37:30 +02:00
snyk-bot
a0854ae35c fix: upgrade @sentry/browser from 5.20.1 to 5.21.1
Snyk has created this PR to upgrade @sentry/browser from 5.20.1 to 5.21.1.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-09-03 03:23:42 +00:00
Son NK
a25559dace able to choose port to listen on in email_handler. Default to 20381 2020-09-02 17:36:11 +02:00
Son NK
d97966a2e8 use warning level when cannot parse mailbox-id 2020-09-02 10:26:46 +02:00
Son NK
5ea3d1bd42 use warning level for when email_log cannot be parsed 2020-09-02 10:25:12 +02:00
Son NK
b7b4c07cd3 use warning level for SMTPRecipientsRefused error 2020-09-02 10:20:04 +02:00
Son NK
c03bb70755 handle SMTPRecipientsRefused in forward phase 2020-09-02 10:16:13 +02:00
Son NK
916e6a1a7f remove whitespace for rcpt in forward phase 2020-09-02 09:58:58 +02:00
Son NK
2d395f99bb make sure to remove whitespace in alias 2020-09-02 09:56:16 +02:00
Son NK
6629b8687b set cookie setup_done=true on the setup_done page 2020-09-01 20:47:57 +02:00
Son NK
f069e6f7cb use warning log for custom domain DNS issue 2020-08-31 17:36:51 +02:00
Son NK
4cb3e54821 send at max 1 email / 30 days for DNS issue on custom domain 2020-08-31 17:36:27 +02:00
Son NK
85b87bbacb improve send_email_with_rate_control to use on any day range 2020-08-31 17:32:46 +02:00
Son NK
e4c4797cdb do not mark a domain as unverified if it fails the MX check 2020-08-31 17:11:18 +02:00
Son NK
63e228d9f4 only alert on invalid mailbox that has too many email logs 2020-08-30 19:59:39 +02:00
Son NK
77c67c5314 create aliases_for_mailbox() and nb_email_log_for_mailbox() 2020-08-30 19:56:45 +02:00
Son NK
8079746e47 handle case where alias mailbox is invalid 2020-08-30 19:22:21 +02:00
Son NK
171100eda7 return 421 in case mailbox is invalid 2020-08-30 19:08:53 +02:00
Son NK
e117726cd9 send alert email when a mailbox is an email alias 2020-08-30 19:06:50 +02:00
Son NK
ffc59a6fad Add check_custom_domain cronjob 2020-08-29 19:05:32 +02:00
Son NK
a3d919db2e take into account mailbox in alias search 2020-08-29 19:03:33 +02:00
Son Nguyen Kim
42d8b017ba
Merge pull request #274 from simple-login/snyk-upgrade-1c7c718062343b000f33520f38177b33
[Snyk] Upgrade @sentry/browser from 5.19.2 to 5.20.1
2020-08-28 12:28:45 +02:00
Son Nguyen Kim
0b2b653a7b
Merge pull request #271 from simple-login/postfix-tls
Enable TLS on Postfix submission
2020-08-28 12:28:23 +02:00
Son NK
62ddaaf7b4 black 2020-08-28 12:27:51 +02:00
Son NK
a3f3c252e3 Move Postfix TLS section to its own file 2020-08-28 12:25:35 +02:00
Son NK
e4271f725c update "reply must from mailbox" email wording 2020-08-27 11:12:48 +02:00
Son NK
a6df989a8f update cycle email subject 2020-08-27 11:10:16 +02:00
Son NK
b937e14ee3 update cycle email text 2020-08-27 10:52:46 +02:00
Son NK
4a90ea9aca send warning about email cycle at most once 2020-08-27 10:43:48 +02:00
Son NK
37a53757eb add send_email_at_most_times 2020-08-27 10:43:30 +02:00
Son NK
03b8a6f2e9 not install pytest & black in github action 2020-08-27 10:24:05 +02:00
Son NK
830299ce2c add black to requirements 2020-08-27 10:23:45 +02:00
Son NK
fdedc24358 black new version 2020-08-27 10:20:48 +02:00
Son NK
bb6e2a35ca send at max 1 email / day for the cycle email issue 2020-08-27 10:16:13 +02:00
Son NK
26ecf38760 use warning log level for cycle email issue 2020-08-27 10:15:40 +02:00
snyk-bot
119622ecb3 fix: upgrade @sentry/browser from 5.19.2 to 5.20.1
Snyk has created this PR to upgrade @sentry/browser from 5.19.2 to 5.20.1.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-08-27 03:23:42 +00:00
Son NK
828d9e4fe1 ignore spoof check if alias.disable_email_spoofing_check 2020-08-26 14:39:51 +02:00
Son NK
9c72f4dec0 Add Alias.disable_email_spoofing_check column 2020-08-26 14:39:03 +02:00
Son NK
4101142253 black 2020-08-26 11:45:24 +02:00
Son NK
f213469e9f display nb-reply, nb-forward on alias contact page 2020-08-26 11:45:07 +02:00
Son Nguyen Kim
a1206d212f
Merge pull request #254 from simple-login/snyk-upgrade-87781a3cbe01c07072c01431e126ec5a
[Snyk] Upgrade @sentry/browser from 5.19.1 to 5.19.2
2020-08-26 05:18:50 -04:00
Son Nguyen Kim
76316c7085
Merge pull request #272 from simple-login/snyk-fix-d563a769e821ae4b433956309ca122f7
[Snyk] Fix for 1 vulnerabilities
2020-08-26 05:18:26 -04:00
snyk-bot
bf7e26d67e fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-SQLALCHEMY-590109
2020-08-25 21:53:34 +00:00
Son NK
cd687664d1 fix email template 2020-08-25 13:03:34 +02:00
Son NK
ab911fd55e do not forward cycle email: email sent to alias from its mailbox 2020-08-25 12:51:19 +02:00
Son NK
ce791567f1 delete header if empty when replacing header 2020-08-25 12:51:19 +02:00
Son NK
61fd81489f Set _EMAIL_LOG_ID_HEADER header for reply phase 2020-08-25 12:51:19 +02:00
Son NK
b53cc94310 set "X-SimpleLogin-Type" header for reply phase 2020-08-25 12:51:19 +02:00
Son NK
d410b34b50 set a custom Message-ID header 2020-08-25 12:51:19 +02:00
Son Nguyen Kim
637bc569eb
Merge pull request #260 from FabioWidmer/improvements-1
Improvements for Self Hosting & More
2020-08-24 20:16:59 +02:00
Son NK
2e6c22131f Enable TLS on Postfix submission 2020-08-24 20:12:43 +02:00
Son NK
9237f43c19 use warning log level for tampered alias 2020-08-24 19:58:21 +02:00
Son NK
0bb10d8fc3 add spam score processing time for forward phase 2020-08-24 18:39:16 +02:00
Son NK
ffa9304d00 log waiting time for get_spam_score 2020-08-24 17:47:56 +02:00
Son NK
9cf807f7bd special handling for case when alias can't be disabled 2020-08-24 10:48:54 +02:00
Son NK
29b6b52a62 try to get email log and mailbox from bounce report 2020-08-24 10:23:49 +02:00
Son NK
17c7303fb5 add get_header_from_bounce() 2020-08-24 10:17:22 +02:00
Son NK
eb6647d62e fix 2020-08-23 20:24:46 +02:00
Son NK
77bfa67402 fix delete_alias: do not use IntegrityError 2020-08-23 20:17:50 +02:00
Fabio Widmer
6a45010740
Remove secret variables from Jinja 2020-08-22 18:38:44 +02:00
Son NK
ef196c5b4a use 5.5 as max spam score 2020-08-22 16:58:51 +02:00
Son NK
00a08d898a refactor: do not use latest_activity in subquery 2020-08-21 23:10:23 +02:00
Son NK
d7583f1733 handle case where alias does not have any activity in get_alias_infos_with_pagination_v3 2020-08-21 20:39:18 +02:00
Son NK
30fe09185f create get_alias_infos_with_pagination_v3 - reduce nb queries used in get_alias_infos_with_pagination_v2 2020-08-21 19:51:48 +02:00
Son NK
06c48244e4 black 2020-08-21 12:03:23 +02:00
Son NK
dc8c2f403e try to fix Message.as_bytes() by trying different policies 2020-08-21 12:01:11 +02:00
Son NK
01afb7557c small refactoring 2020-08-21 10:47:10 +02:00
Son NK
2b2512e775 no need to create a copy of message when there's only 1 mailbox 2020-08-21 10:41:50 +02:00
Son NK
5bb4c20fba fix nb_bounced computation 2020-08-21 10:32:10 +02:00
Son NK
90eae05e9e better logging 2020-08-21 10:20:08 +02:00
Son NK
386fcbdc3a refactoring 2020-08-21 10:18:58 +02:00
Son NK
a3e052cc7b black 2020-08-20 14:28:57 +02:00
Son NK
db0e197500 set the email log that has been bounced 2020-08-20 14:27:05 +02:00
Son NK
1de57119c4 add missing commit 2020-08-20 11:58:46 +02:00
Son NK
c77b0c07b4 fix alias sorting 2020-08-20 10:11:41 +02:00
Son NK
b8e0ee424c pass userId to Paddle 2020-08-20 09:44:45 +02:00
Son NK
7e345e4db3 fix typo 2020-08-19 10:03:07 +02:00
Son NK
7f34dc1a20 remove auto email fill-up on paddle 2020-08-19 09:22:54 +02:00
Fabio Widmer
aa18b7ecd1
Add variables to Jinja 2020-08-18 08:47:41 +02:00
Son NK
8626c5e232 add cronjob to delete old monitoring records 2020-08-17 14:02:59 +02:00
Son NK
50683be4f8 increase spamassassin timeout to 300s 2020-08-17 11:42:46 +02:00
Son NK
38bf117f29 move the lock sync to _handle 2020-08-17 11:40:58 +02:00
Son NK
d8a415c00a do not use aiostmpd controller 2020-08-17 11:39:13 +02:00
Son NK
24d8babe46 handle case spamassassin can't be reached 2020-08-16 21:54:19 +02:00
Son NK
735c0310fd install utility packages in dockerfile 2020-08-16 19:34:20 +02:00
Son NK
284aaad52b fix .dockerignore 2020-08-16 19:34:08 +02:00
Son NK
cbdd080587 revert port binding on email handler 2020-08-16 19:34:00 +02:00
Son NK
6835c5b69d handle case cannot parse mailbox_id 2020-08-16 18:55:14 +02:00
Son NK
a3a99ac3f4 use lock to handle 1 email at a time 2020-08-16 18:51:12 +02:00
Son NK
d99d186bc0 revert the ignore AssertionError commit 2020-08-16 18:50:20 +02:00
Son NK
61a8f1e676 ignore the "Popped wrong app context" Assertion Error raised by AppContext 2020-08-16 15:34:44 +02:00
Son NK
d4a6269e43 bind email handler to 127.0.0.1 instead of 0.0.0.0 2020-08-16 14:51:40 +02:00
Son NK
98a9e88ce4 fix spamassassin: add ending linebreak 2020-08-16 14:34:50 +02:00
Son NK
1ab9c926dd set email_log.spam_score 2020-08-16 14:28:47 +02:00
Fabio Widmer
28dbafe1f7
Hide deprecated social login if not used 2020-08-16 12:57:12 +02:00
Son NK
0c6a5f4333 add EmailLog.spam_score column 2020-08-16 11:59:53 +02:00
Son NK
d738997c4e return 421 if any unexpected error happen 2020-08-16 11:10:01 +02:00
Son NK
118862ead0 only alert if monitoring fails >3 times consecutive 2020-08-16 10:27:35 +02:00
Son NK
79853b7736 use a timeout for get_spam_score 2020-08-16 10:22:16 +02:00
Son NK
3d638f1a97 extract monitoring to its own file 2020-08-15 19:55:56 +02:00
Son Nguyen Kim
becb3fe720
Merge pull request #264 from simple-login/spamassassin
Enable Spamassassin server
2020-08-15 17:03:38 +02:00
Son NK
359eec23c0 take into account spam email during reply phase on refused email page 2020-08-15 16:58:11 +02:00
Son NK
f9300009e5 refactor: rename forward -> contact 2020-08-15 16:56:16 +02:00
Son NK
bf555ed605 detect spam in reply phase 2020-08-15 16:53:57 +02:00
Son NK
673b08712c use SPAMASSASSIN server if available 2020-08-15 16:38:16 +02:00
Son NK
c4dd980cf6 add aiospamc to requirement 2020-08-15 16:35:18 +02:00
Son NK
08db23658a add SPAMASSASSIN_HOST param 2020-08-15 16:33:48 +02:00
Son NK
ec0a2bb6e3 fix crontab 2020-08-15 16:33:34 +02:00
Son NK
2a38d7c5fa update Postfix TLS config: use smtpd_tls_security_level instead of smtpd_use_tls, add smtp_tls_security_level 2020-08-15 13:17:41 +02:00
Son NK
8e6fb9975d add sql migration 2020-08-15 13:17:08 +02:00
Son NK
4bbb07c3ce add monitoring cronjob that monitors how many emails in Postfix queues 2020-08-15 13:16:51 +02:00
Son NK
c0f263ee70 add Monitoring model 2020-08-15 13:15:20 +02:00
Son NK
d8e4396a70 add HOST param 2020-08-14 19:09:45 +02:00
Fabio Widmer
fbb17b1f57
Fix email_utils.py formatting 2020-08-14 16:50:59 +02:00
Fabio Widmer
9fa9dbe821
Fix email_utils.py formatting 2020-08-14 16:30:43 +02:00
Fabio Widmer
9226492f83
More self hosting improvements 2020-08-14 16:23:29 +02:00
Son Nguyen Kim
edb8144be8
Merge pull request #263 from simple-login/fix-alias-trash
Fix alias trash
2020-08-14 12:38:33 +02:00
Son NK
89830e2173 fix import 2020-08-14 12:06:26 +02:00
Son NK
5636b7ba32 do not use DomainDeletedAlias.create 2020-08-14 12:03:59 +02:00
Son NK
6bc0c5ada4 fix Mailbox.delete: use alias_utils.delete_alias 2020-08-14 12:03:41 +02:00
Son NK
b4eb110971 fix Directory.create: use alias_utils.delete_alias 2020-08-14 12:02:54 +02:00
Son NK
2cae0200a8 use delete_alias(alias,user) instead of Alias.delete, DomainDeletedAlias.create, DeletedAlias.create 2020-08-14 12:02:33 +02:00
Son NK
4434ad62dd handle the case 2 users want to use the same email address 2020-08-13 11:00:48 +02:00
Son NK
b7cbaa6e84 delete the expired ChangeEmail object 2020-08-13 10:59:39 +02:00
Son Nguyen Kim
cb687c4248
Merge pull request #259 from FabioWidmer/plausible-analytics-support
Plausible Analytics Support
2020-08-12 20:25:37 +02:00
Fabio Widmer
8e71e8e7f4
A few (self hosting) improvements 2020-08-12 16:12:41 +02:00
Fabio Widmer
d0ed69f8aa
Add Plausible Analytics support 2020-08-12 15:24:34 +02:00
Son NK
0d1e5b1f7d Revert "show alert message on the email delay issue"
This reverts commit cba78b1b5d.
2020-08-12 12:49:09 +02:00
Son NK
b395c2ebd0 use warning log for FIDO error 2020-08-12 12:48:51 +02:00
Son NK
7e5deef34f add time measures to email_handler 2020-08-11 17:32:04 +02:00
Son NK
cba78b1b5d show alert message on the email delay issue 2020-08-11 17:04:00 +02:00
Son NK
00411cef61 reduce further 2020-08-11 16:31:08 +02:00
Son NK
dc206b41c7 lighten mail handler 2020-08-11 16:31:08 +02:00
Son NK
184397dc92 remove sentry AioHttpIntegration 2020-08-11 08:45:06 +02:00
Son NK
ef45e28ab3 Add aiocontextvars to fix "RuntimeError: The aiohttp integration for Sentry requires Python 3.7+ or aiocontextvars package" 2020-08-11 08:44:06 +02:00
Son NK
b064341f4e install gevent 2020-08-11 08:28:48 +02:00
Son NK
9def7df974 also search for PGP key in contact 2020-08-08 10:26:24 +02:00
Son NK
8db2ddcd5b Add nb_apple_premium, take into account canceled subscription in nb_premium 2020-08-07 10:06:00 +02:00
Son NK
8a11e42da9 notify admin when user cancels 2020-08-07 10:01:11 +02:00
Son NK
c74857c7e7 use alias name when searching 2020-08-07 09:56:44 +02:00
Son NK
2f00294ba3 replace pgp_enabled by disable_pgp 2020-08-06 14:22:28 +02:00
Son NK
0484fdbb83 update doc 2020-08-05 20:59:24 +02:00
Son NK
5dc631a6b5 black 2020-08-05 12:31:08 +02:00
Son NK
1f0ef13ff2 do not require user to re-enter TOTP code when cancelling TOTP 2020-08-05 12:30:56 +02:00
Son NK
f17608df50 add link back to home page in recovery page 2020-08-05 12:28:20 +02:00
snyk-bot
8e3a16841d fix: upgrade @sentry/browser from 5.19.1 to 5.19.2
Snyk has created this PR to upgrade @sentry/browser from 5.19.1 to 5.19.2.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-08-05 03:23:41 +00:00
Son Nguyen Kim
5d61d5b31b
Merge pull request #252 from simple-login/fix-custom-domain
avoid adding a built-in domain as custom domain
2020-08-04 21:09:42 +02:00
Son NK
6cb589350b avoid adding a built-in domain as custom domain 2020-08-04 21:07:45 +02:00
Son Nguyen Kim
0a50b21450
Merge pull request #251 from simple-login/pgp-api
Pgp api
2020-08-04 20:22:35 +02:00
Son NK
a3051b3d45 black 2020-08-04 20:12:15 +02:00
Son NK
b3ca7d1d5b Return pgp_enabled in GET /api/v2/aliases 2020-08-04 20:11:59 +02:00
Son NK
03841693ba Return support_pgp in GET /api/v2/aliases 2020-08-04 20:09:42 +02:00
Son NK
3d2a325e55 enable debug toolbar when running locally 2020-08-04 11:37:59 +02:00
Son NK
f2fb599664 Optimize Alias.get_contacts() to retrieve the latest reply for each contact in a single query 2020-08-04 11:37:59 +02:00
Son Nguyen Kim
a0ed091b35
Merge pull request #248 from simple-login/snyk-upgrade-b52e398d97b584f86cb8ae3654e99692
[Snyk] Upgrade @sentry/browser from 5.19.0 to 5.19.1
2020-08-02 10:05:38 +02:00
snyk-bot
6d142cc926 fix: upgrade @sentry/browser from 5.19.0 to 5.19.1
Snyk has created this PR to upgrade @sentry/browser from 5.19.0 to 5.19.1.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-08-02 03:24:26 +00:00
Son Nguyen Kim
852c13fb60
Merge pull request #247 from simple-login/domain-mailbox
Domain mailbox
2020-08-01 12:54:34 +02:00
Son NK
4a2a4b9828 migration script 2020-08-01 12:48:03 +02:00
Son NK
918b18870f show mailboxes that a catch-all alias belongs to 2020-08-01 12:41:48 +02:00
Son NK
41e2283d93 domain catch-all alias belongs to domain mailboxes 2020-08-01 12:31:43 +02:00
Son NK
f5bc166f39 able to choose mailboxes for a domain 2020-08-01 12:31:02 +02:00
Son NK
ec8f120085 small fixes in directory.py 2020-08-01 12:22:52 +02:00
Son NK
e8fc9752b5 Add DomailMailbox model 2020-08-01 12:20:59 +02:00
Son NK
d98cde440a add how to test sending email using swaks and mailcatcher 2020-08-01 12:20:15 +02:00
Son Nguyen Kim
9d78f9a21b
Merge pull request #244 from simple-login/snyk-upgrade-db7b068f1af8d53cc31b15d0c627a8b3
[Snyk] Upgrade @sentry/browser from 5.12.0 to 5.19.0
2020-08-01 11:03:32 +02:00
Son Nguyen Kim
c8a4c53870
Merge pull request #246 from simple-login/fix-ai-key
fix api key counter not correctly incremented
2020-08-01 10:15:25 +02:00
Son NK
ed22f5116f fix api key counter not correctly incremented 2020-08-01 10:14:59 +02:00
Son NK
3e5323c2dd add no-referrer for referrer meta 2020-07-30 10:09:26 +02:00
Son NK
68eeb2e121 add rel="noopener" for target="_blank" link 2020-07-30 10:09:10 +02:00
Son NK
701579f18c update opencollective handle 2020-07-30 08:27:38 +02:00
snyk-bot
327a26b5d1 fix: upgrade @sentry/browser from 5.12.0 to 5.19.0
Snyk has created this PR to upgrade @sentry/browser from 5.12.0 to 5.19.0.

See this package in npm:
https://www.npmjs.com/package/@sentry/browser

See this project in Snyk:
https://app.snyk.io/org/nguyenkims/project/72f25afd-ac84-4504-a9bd-dc5ead29b930?utm_source=github&utm_medium=upgrade-pr
2020-07-25 03:23:46 +00:00
Son Nguyen Kim
0121806301
Merge pull request #232 from simple-login/fix-doc
Fix doc
2020-07-23 14:15:32 +02:00
Son Nguyen Kim
c78e3a6ee2
Merge branch 'master' into fix-doc 2020-07-23 14:15:24 +02:00
Son Nguyen Kim
782844e2b9
Merge pull request #243 from simple-login/hcaptcha
Ask for Hcaptcha on sign up page if enabled
2020-07-23 14:14:25 +02:00
Son NK
efe1ab641f add hCaptcha check 2020-07-23 12:43:55 +02:00
Son NK
307e3c93c6 Add HCAPTCHA_SECRET, HCAPTCHA_SITEKEY param 2020-07-23 12:40:50 +02:00
Son NK
38a6bcc461 add warning log 2020-07-23 11:32:11 +02:00
Son NK
7a22b58f19 migration file 2020-07-23 11:11:54 +02:00
Son NK
d89e41d0e5 use user.max_spam_score if present 2020-07-23 11:11:43 +02:00
Son NK
8e9968a7d9 Add User.max_spam_score column 2020-07-23 11:09:28 +02:00
Son NK
6faaacc972 add more logging 2020-07-23 10:32:10 +02:00
Son NK
582c92bbcd add opencollective 2020-07-21 20:58:11 +02:00
Son NK
363bfa1bf3 Add LOCAL_FILE_UPLOAD option to README 2020-07-18 20:14:52 +02:00
Son NK
6437ee46e0 use LOG.exception instead of LOG.error to provide stacktrace 2020-07-17 12:59:07 +02:00
Son NK
7f29756230 do not hardcode oauth urls 2020-07-16 23:48:47 +02:00
Son NK
515b3510a8 use warning when user is out of quota 2020-07-15 19:14:37 +02:00
Son NK
8f17cda794 use warning error for alias expiration error 2020-07-13 20:40:26 +02:00
Son NK
bb343a5cde use warning level for hit rate limit error 2020-07-11 19:28:54 +02:00
Son NK
28c96d0d35 return 412 when alias ceration time is expired 2020-07-11 19:23:56 +02:00
Son NK
293a5cb396 update email template wording 2020-07-11 19:23:40 +02:00
Son NK
81bc11bd8c add init data step to README 2020-07-06 11:05:26 +02:00
Son Nguyen Kim
7ab04b030e
Merge pull request #235 from simple-login/error-info
add more info to error message
2020-07-05 18:15:29 +02:00
Son NK
7a20261aae add more info to error message 2020-07-05 16:25:54 +02:00
Son NK
1bbc416ec1 fix the case public domain is also a custom domain for a specific user: check for public domain first. 2020-07-05 11:45:00 +02:00
Son NK
f5e6f541ff fix email templates 2020-07-05 11:44:03 +02:00
Son Nguyen Kim
5e464a824c
Merge pull request #234 from simple-login/random-alias-domain
Random alias domain
2020-07-04 23:34:33 +02:00
Son NK
d7b992aef3 sql migration 2020-07-04 23:32:55 +02:00
Son NK
74498146d8 add public domain in fake_data 2020-07-04 23:29:15 +02:00
Son NK
f9cb40aa5b user can choose a random alias domain in a list of public domains 2020-07-04 23:29:06 +02:00
Son NK
4b479ea003 take into account user.default_random_alias_public_domain_id in create_new_random() 2020-07-04 23:27:02 +02:00
Son NK
4542a8353e add User.available_domains_for_random_alias(), default_random_alias_domain() 2020-07-04 23:24:32 +02:00
Son NK
955231199e Add User.default_random_alias_public_domain_id 2020-07-04 23:23:39 +02:00
Son NK
dc9ee048a2 add all domain in ALIAS_DOMAINS to public domain table 2020-07-04 23:18:30 +02:00
Son NK
ee9b796b7d Add PublicDomain model 2020-07-04 23:18:11 +02:00
Son NK
4cd0227477 refactoring: move model methods below fields 2020-07-04 22:46:09 +02:00
Son NK
5ed39b47ca add setup-done page 2020-07-04 19:42:48 +02:00
Son NK
d59e9a6238 use warning level for apple server issue 2020-07-04 18:24:34 +02:00
Son NK
d0776b770f add GET /api/logout 2020-07-04 12:10:04 +02:00
Son NK
0d3a3e0c48 Create POST /api/api_key 2020-07-04 11:41:31 +02:00
Son NK
5b3ec91300 login user in api auth endpoints 2020-07-04 10:39:38 +02:00
Son NK
df96773959 show nb paid user on referral page 2020-07-02 08:48:59 +02:00
Son NK
96ac15a3e0 add user.is_paid() and referral.nb_paid_user() 2020-07-02 08:48:37 +02:00
Son NK
40b4273111 take into account /verifyReceipt can return 5** 2020-06-30 20:10:12 +02:00
Son NK
3754cee8f8 ignore venv/ 2020-06-30 20:10:03 +02:00
Son NK
e3569ee7ad create less aliases in fake_data 2020-06-30 20:09:48 +02:00
Son NK
c22af6d1f0 no need to bind to 0.0.0.0 2020-06-28 21:58:08 +02:00
Son NK
c41bffbbae refactor: use SESSION_COOKIE_NAME instead of hardcoding "slapp" 2020-06-28 21:17:18 +02:00
Son NK
8e068eea30 fix user has to login again after quitting the browser 2020-06-28 21:14:30 +02:00
Son NK
59764f8e84 sql migration 2020-06-28 11:18:44 +02:00
Son NK
28da78e75f sanity_check: alert when too many checks fail on a mailbox 2020-06-28 11:18:06 +02:00
Son NK
b23259cacd add Mailbox.nb_failed_checks 2020-06-28 11:17:36 +02:00
Son NK
ec0f5ccd3a only return verified mailbox in alias.mailboxes 2020-06-28 11:15:29 +02:00
Son NK
204f5f9a0c fix alias can be none in delete alias endpoint 2020-06-28 09:48:21 +02:00
Son NK
02bd45bc4a fix alias can be none in update alias endpoint 2020-06-28 09:48:21 +02:00
Son Nguyen Kim
0e3aa42326
Merge pull request #233 from simple-login/random-alias-custom-domain
Random alias custom domain
2020-06-25 13:07:35 +02:00
Son NK
1f7779ed7b add sql migration 2020-06-25 13:05:37 +02:00
Son NK
b2f82ba4a8 use custom domain to generate a random alias if user enables this option 2020-06-25 13:05:25 +02:00
Son NK
abe9768db4 user can update the random alias domain 2020-06-25 13:04:27 +02:00
Son NK
040c6d1f9e add User.default_random_alias_domain_id 2020-06-25 13:02:43 +02:00
Son NK
c91b44fa97 set rate limit to 5/minute on new alias routes 2020-06-24 10:32:22 +02:00
Son NK
774ffcae3b enable CORS on /api endpoints 2020-06-24 10:30:01 +02:00
Son NK
85bb30abb0 Notify user can reply cannot be sent 2020-06-20 16:19:01 +02:00
Son NK
45a8b360e4 handle case where alias is None 2020-06-19 23:44:16 +02:00
Son NK
edfd3c0719 add disable_mailbox to shell 2020-06-19 23:41:16 +02:00
Son NK
3f8f306a34 enable LOCAL_FILE_UPLOAD by default 2020-06-19 20:01:37 +02:00
Son NK
13416bfd31 Add and mount ./sl/upload directory 2020-06-19 20:01:20 +02:00
Son NK
3d0d42c8b3 v3.2.2 2020-06-15 23:36:40 +02:00
Son NK
ee6a1a672d fix POST /v2/alias/custom/new when DISABLE_ALIAS_SUFFIX is set 2020-06-15 23:35:20 +02:00
Son NK
6f820c5fc5 add /env to .gitignore 2020-06-15 23:32:31 +02:00
Son NK
220c7bdf11 v3.2.1 2020-06-15 16:59:43 +02:00
Son NK
1a22d0cf9b fix domain can be null in self-hosting
# Conflicts:
#	app/api/views/new_custom_alias.py
#	app/dashboard/views/custom_alias.py
2020-06-15 15:58:05 +02:00
Son NK
4906d3a4a8 add more logging in email_domain_can_be_used_as_mailbox 2020-06-14 12:00:02 +02:00
Son NK
84381e9635 reduce memory use in cron by using yield_per() 2020-06-12 23:50:21 +02:00
Son NK
b79933ba4c sleep between mailbox check 2020-06-12 00:09:53 +02:00
Son NK
c61e7c697d call forward_email_to_mailbox on the msg copy 2020-06-12 00:02:45 +02:00
Son NK
5705842415 add email_utils.copy() 2020-06-12 00:02:07 +02:00
Son NK
1dc2e9c54f fix retry pgp 2020-06-12 00:01:21 +02:00
Son NK
2ef33cc23f enable sanity check 2020-06-11 23:36:15 +02:00
Son NK
987b413e3d check if user and mailbox email address are lowercase in sanity_check 2020-06-11 23:36:06 +02:00
Son NK
df47ea1983 do not disable mailbox in sanity_check 2020-06-11 23:35:44 +02:00
Son NK
4ee38823b8 make sure to strip and lower email 2020-06-11 23:35:24 +02:00
Son NK
cadbe7d32b order alias.mailboxes 2020-06-10 22:32:00 +02:00
Son NK
9e2f1c5f9f try to load the public key if encrypt fails for 1st time 2020-06-10 22:28:15 +02:00
Son Nguyen Kim
2034225a37
Merge pull request #226 from simple-login/sender-report
Handle transactional bounce emails
2020-06-10 13:59:49 +02:00
Son NK
9c9319c94e handle emails sent to sender 2020-06-10 13:57:23 +02:00
Son NK
d0c65ea378 send transactional email from SENDER if set 2020-06-10 13:55:47 +02:00
Son NK
9abfa3e98c Add new param SENDER, SENDER_DIR 2020-06-10 13:54:42 +02:00
Son NK
b47d95226d generate html from plaintext if not set 2020-06-10 12:18:39 +02:00
Son NK
0c4e48c906 remove bounced_email param from send_email_with_rate_control 2020-06-10 12:17:04 +02:00
Son NK
da7b46ef97 remove bounced_email param from send_email 2020-06-10 12:15:57 +02:00
Son NK
c36870daa8 force convert header to string if needed 2020-06-10 09:34:58 +02:00
Son NK
c847d205b6 v3.2.0 2020-06-10 09:22:05 +02:00
Son NK
bee648b6b5 update changelog 2020-06-10 09:19:52 +02:00
Son NK
bf596280e4 disable sanity check for now as it's buggy! 2020-06-09 19:56:17 +02:00
Son NK
a255c2652e fix the email 2020-06-09 18:58:42 +02:00
Son NK
dfe708b4fb return user email in /api/auth/mfa 2020-06-09 17:20:37 +02:00
Son NK
0002531bc0 return user email in /api/auth/login 2020-06-09 17:19:03 +02:00
Son NK
53e9281204 avoid forward email to invalid mailbox 2020-06-09 17:16:32 +02:00
Son NK
befbb0c0c0 Add sanity_check that disables invalid mailbox and all of its aliases 2020-06-09 17:12:34 +02:00
Son NK
1101ba5afa add Mailbox.nb_email_log and aliases 2020-06-09 17:02:45 +02:00
Son NK
582a971b80 remove pgp retry mechanism 2020-06-08 23:05:35 +02:00
Son NK
9b1ca0a2f1 return 421 when pgp encryption fails 2020-06-08 13:54:42 +02:00
Son NK
e988573cb4 hard exit when memory is more than 300MB 2020-06-08 13:53:27 +02:00
Son Nguyen Kim
7a3a6784cc
Merge pull request #224 from simple-login/contact-pgp
Contact pgp
2020-06-07 13:48:40 +02:00
Son NK
d85b32d56f prettify contact manager page 2020-06-07 13:41:59 +02:00
Son NK
08f4891492 use breadcrumb for contact header 2020-06-07 13:41:59 +02:00
Son NK
6cccb450b0 fix contact order on alias contact manager page 2020-06-07 13:41:59 +02:00
Son NK
7b2d86552b fix popup display when edit contact 2020-06-07 13:41:59 +02:00
Son NK
bc464d3549 migration script 2020-06-07 13:41:59 +02:00
Son NK
f708ee6bb2 improve wordings in alias contact manager page 2020-06-07 13:41:59 +02:00
Son NK
016d342f3b load contact pgp keys in load_pgp_public_keys 2020-06-07 13:41:59 +02:00
Son NK
b962d6a2c1 encrypt sent email if contact has PGP enabled 2020-06-07 13:40:24 +02:00
Son NK
afe975b8c3 User can add PGP key to for a contact 2020-06-07 13:40:24 +02:00
Son NK
c593253c7d Add pgp_public_key,pgp_finger_print columns to Contact model 2020-06-07 13:40:24 +02:00
Son Nguyen Kim
49a81db951
Merge pull request #223 from simple-login/pgp-fix
Fix intermitten PGP errors
2020-06-07 13:38:37 +02:00
Son NK
ce4992c7fb check if alias exists 2020-06-07 12:50:30 +02:00
Son NK
f4beb81195 refactor load_pgp_public_keys 2020-06-07 12:46:59 +02:00
Son NK
16df2acb29 stop the email handler process when PGP error 2020-06-07 11:41:35 +02:00
Son NK
123f3583fd log memory usage in encrypt_file() 2020-06-07 10:23:36 +02:00
Son NK
3ceef5bd66 Install memory_profiler 2020-06-07 09:44:42 +02:00
Son NK
708816cb05 use warning level for SPF fail message 2020-06-06 23:38:19 +02:00
Son NK
c8cd066d25 try encrypt again if fail 2020-06-06 23:06:34 +02:00
Son NK
e5b60d9251 prettify directory page 2020-06-05 23:25:37 +02:00
Son NK
8b066fdf3d missing migration 2020-06-05 23:10:34 +02:00
Son Nguyen Kim
4eef620e3c
Merge pull request #222 from simple-login/directory-mailboxes
Directory mailboxes
2020-06-05 22:33:01 +02:00
Son NK
83e540d1d4 Create directory alias with directory mailboxes 2020-06-05 22:30:32 +02:00
Son NK
bc01479a72 user can update directory mailboxes 2020-06-05 22:13:35 +02:00
Son NK
18b530fe6f Able to set mailboxes when creating directory 2020-06-05 22:12:21 +02:00
Son NK
8161d89c39 Create DirectoryMailbox model 2020-06-05 22:08:08 +02:00
Son NK
9603683c18 log the failed encrypted content to debug 2020-06-05 10:10:21 +02:00
Son NK
50a7442d02 update notification email 2020-06-03 21:37:44 +02:00
Son NK
89f200fbc6 format 2020-06-03 21:32:37 +02:00
Son NK
ecab3ea6ed take into account the case premium user obtains a lifetime license
- do not show subscription expired date
- show lifetime plan message on settings page
2020-06-03 21:32:15 +02:00
Son NK
9fc0748fcc Support setting alias name in POST /api/v3/alias/custom/new 2020-06-03 21:22:29 +02:00
Son NK
d76aad3f17 format 2020-06-03 20:05:05 +02:00
Son NK
89dd8663ce fix alias is None 2020-06-03 20:04:54 +02:00
Son NK
acac06188c fix mailbox.nb_alias(): take into account multiple mailboxes per alias case 2020-06-03 09:06:38 +02:00
Son Nguyen Kim
f852bc508b
Merge pull request #221 from simple-login/custom-alias-api-v3
Create POST /api/v3/alias/custom/new
2020-06-02 20:07:22 +02:00
Son NK
48dc0dd1cc change mailboxes to mailbox_ids 2020-06-02 20:06:32 +02:00
Son NK
d055989239 Create POST /api/v3/alias/custom/new 2020-06-02 09:33:56 +02:00
Son NK
b356ea1b28 force convert header to string, sometimes addrs is Header object 2020-06-01 21:14:23 +02:00
Son NK
578d63541e Add paypal to funding 2020-06-01 11:45:44 +02:00
Son NK
48998ff07e return nb_alias in GET /mailboxes 2020-05-31 11:52:01 +02:00
Son NK
0530a8aab5 return creation_timestamp in GET /mailboxes 2020-05-31 11:49:53 +02:00
Son Nguyen Kim
28287ff93e
Merge pull request #212 from SibrenVasse/multipart_fix
Fix reverse alias replacement multipart message
2020-05-30 20:04:22 +02:00
Son Nguyen Kim
677236b9a6
Merge pull request #218 from simple-login/not-reuse-password
make sure user cannot reuse the old password
2020-05-30 19:57:47 +02:00
Son NK
fa06c5cd4b make sure user cannot reuse the old password 2020-05-30 19:50:33 +02:00
Son Nguyen Kim
1e00ea300a
Merge pull request #214 from FozzieHi/reword
Reword some sentences.
2020-05-29 19:59:39 +02:00
George
a9460f120b
Change 2FA text 2020-05-29 13:51:56 +01:00
George
d10a993e9d
Fix typo 2020-05-29 13:46:50 +01:00
George
b9fd211acb
Fix lifetime user message 2020-05-29 13:46:06 +01:00
George
007768a5bb
User is able to view recovery codes. 2020-05-29 13:34:14 +01:00
George
c28484130b
Fix 2FA message 2020-05-29 13:33:14 +01:00
George
1c57dca4f3
Fix 2FA message 2020-05-29 13:32:55 +01:00
George
8635b37281
Add catch-all domains 2020-05-29 13:32:15 +01:00
George
9af4a6949a
Fix API key wording 2020-05-29 13:31:20 +01:00
Son NK
dc9701177f migration script 2020-05-28 20:38:38 +02:00
Son NK
683b3e54d8 remove User.can_use_fido column: anyone can setup FIDO 2020-05-28 20:38:29 +02:00
Sibren Vasse
5c50628d36 Fix reverse alias replacement multipart message 2020-05-28 20:25:51 +02:00
George
a87f7e4be9
Change words 2020-05-27 21:53:48 +01:00
George
6f78802c0a
Rename text box requirements. 2020-05-27 21:52:45 +01:00
Son NK
e0117e3d67 v3.1.1 2020-05-27 22:18:04 +02:00
George
d9e29cc989
Reword 2FA page. 2020-05-27 19:49:13 +01:00
George
972f651eca
Reword pricing page. 2020-05-27 19:45:29 +01:00
George
aa8a8fafff
Change plan text. 2020-05-27 19:34:25 +01:00
Son Nguyen Kim
4b42294cca
Merge pull request #215 from simple-login/fix-fido-setup-firefox
Fix fido setup not working on Firefox
2020-05-27 20:28:53 +02:00
Son NK
7a708ec41b Fix fido setup not working on Firefox
- disable form automatic submit when entering
- replace button by span to avoid automatic submit when clicking on the button on firefox
- check if key name is not empty
2020-05-27 20:28:31 +02:00
George
8c6cce9051
Change plan wording. 2020-05-27 19:04:16 +01:00
George
fd58557f19
Reword the settings page. 2020-05-27 18:48:05 +01:00
George
299e51cc08
Reword some sentences. 2020-05-27 18:23:33 +01:00
Son NK
78d9a88328 Add default field to GET /api/mailboxes 2020-05-27 14:18:20 +02:00
Son NK
0367ab0e78 migration script 2020-05-27 14:12:44 +02:00
Son NK
c8c06aa10e do not automatically disable alias if it cannot be disabled 2020-05-27 14:12:32 +02:00
Son NK
2fb4b38e28 add alias.cannot_be_disabled 2020-05-27 14:11:32 +02:00
Son NK
5b1d8d4d74 migration script 2020-05-27 00:20:33 +02:00
Son NK
eb6bfdd56f only show upgrade button to "free" lifetime user 2020-05-27 00:20:09 +02:00
Son NK
2f0a5aa429 add user.paid_lifetime column 2020-05-27 00:18:45 +02:00
Son NK
ba5d71dd75 Use FIRST_ALIAS_DOMAIN instead of EMAIL_DOMAIN when creating random alias 2020-05-26 09:07:36 +02:00
Son NK
62017592e1 Add ALIAS_DOMAINS config 2020-05-25 19:51:30 +02:00
Son Nguyen Kim
7d0ab3651f
Merge pull request #209 from SibrenVasse/rate_limit
Implement rate limiting
2020-05-25 12:18:19 +02:00
Sibren Vasse
31a1f94a5f Implement rate limiting 2020-05-25 11:39:33 +02:00
Son NK
b65328afe7 workaround browser cache for webauthn.js 2020-05-25 00:10:12 +02:00
Son NK
c68fad741b Improve how to use directory 2020-05-24 23:55:49 +02:00
Son NK
22c5513909 fix ForeignKeyViolation 2020-05-24 20:35:52 +02:00
Son NK
eff960b451 regenerate migration 2020-05-24 19:54:12 +02:00
Son Nguyen Kim
93b7ff3d28
Merge pull request #203 from SibrenVasse/remember_mfa
Optional 'remember MFA' for browser
2020-05-24 19:52:11 +02:00
Sibren Vasse
3c7e03f83d Add remember option to FIDO mfa path 2020-05-24 19:23:16 +02:00
Sibren Vasse
097ac771b0 Prevent OTP replay attacks by invalidating last token 2020-05-24 19:23:16 +02:00
Sibren Vasse
35bb1645a3 Allow user to disable mfa for browser for 30 days 2020-05-24 19:23:16 +02:00
Sibren Vasse
e15ab7f932 Add autofocus to login screen 2020-05-24 19:23:16 +02:00
Sibren Vasse
8c946d7026 Remove token when submitted value is incorrect 2020-05-24 19:23:16 +02:00
Son NK
de1823c854 add migration script 2020-05-24 19:04:01 +02:00
Son Nguyen Kim
eb60028b1f
Merge pull request #199 from developStorm/webauthn-multiple-keys
Support Multiple Keys for WebAuthn
2020-05-24 18:56:42 +02:00
Son NK
44fbffb679 add how to use for contact page 2020-05-23 23:21:00 +02:00
Son NK
0b1131a412 improve wording 2020-05-23 23:20:47 +02:00
Son NK
dcca3f0459 make introjs compatible with dark mode 2020-05-23 23:03:38 +02:00
Son Nguyen Kim
2d45b324e8
Merge pull request #208 from simple-login/notif-improve
Improve the notification
2020-05-23 23:02:33 +02:00
Son NK
3c6d137bf1 Only show notification icon when there's at least 1 notification. Only show "load more" when there's more 🙄 2020-05-23 22:52:35 +02:00
Son NK
1e03f26cfa Return whether there's more notification in GET /api/notifications 2020-05-23 22:51:00 +02:00
Son NK
c47fb44c1e Pagination for contact page 2020-05-23 22:34:46 +02:00
Son NK
1d62400aac move darkmode.css to static/ and fix dark mode on notification 2020-05-23 21:11:53 +02:00
Son Nguyen Kim
d371b2ec2d
Merge pull request #207 from simple-login/notification
Notification
2020-05-23 20:55:00 +02:00
Son NK
01a40ff801 migration script 2020-05-23 19:54:39 +02:00
Son NK
b01c91423f display notifications. user can mark a notification as read. 2020-05-23 19:54:06 +02:00
Son NK
95c5cb8452 move StartIntro() to footer.html 2020-05-23 19:54:06 +02:00
Son NK
dae357dd6b Add GET /api/notifications, /api/notifications/:notification_id 2020-05-23 19:54:06 +02:00
Son NK
a2e7de0bab Add Notification model 2020-05-23 19:54:06 +02:00
Son NK
dfb427e6da format 2020-05-23 19:50:36 +02:00
Son NK
9bb17533c1 no need to check for deletedAlias when changing mailbox or user email 2020-05-23 19:50:04 +02:00
Son NK
adce27b88b Add DomainDeletedAlias.get_by check when creating custom alias 2020-05-23 19:49:40 +02:00
Son NK
605e8d1f23 Fix DomainDeletedAlias check in Alias.create 2020-05-23 19:48:45 +02:00
Son NK
a4f8dc9c9d Use AliasInTrashError instead of DeletedAlias.get_by check when trying to create alias automatically 2020-05-23 19:45:26 +02:00
Son NK
c73820920b check DomainDeletedAlias when creating new alias 2020-05-23 19:35:18 +02:00
Son Nguyen Kim
1ded0c3e26
Merge pull request #206 from simple-login/normalize-api-response
Normalize api response
2020-05-23 19:33:13 +02:00
Son NK
1d598252e7 format 2020-05-23 19:18:50 +02:00
Son NK
6fc380c0d9 remove unused imports 2020-05-23 19:18:35 +02:00
Son NK
42b3666f45 use the alias v2 format for GET /api/aliases/:alias_id, POST /api/v2/alias/custom/new, POST /api/v2/alias/random/new 2020-05-23 19:18:24 +02:00
Son Nguyen Kim
8777db0729
Merge pull request #205 from simple-login/mailbox-api
Add endpoints for mailbox
2020-05-23 16:51:01 +02:00
Son NK
6280512adf move get mailboxes to mailbox.py 2020-05-23 16:46:10 +02:00
Son NK
3eb6700232 user can cancel mailbox email change 2020-05-23 16:43:48 +02:00
Son NK
2f087de061 can update mailbox email 2020-05-23 16:40:28 +02:00
Son NK
a76ad0485f PUT /api/mailboxes/:mailbox_id: update mailbox 2020-05-23 16:26:26 +02:00
Son NK
5ae39c85c6 Add DELETE /api/mailboxes/:mailbox_id 2020-05-23 16:18:12 +02:00
Son NK
722bff319e add POST /api/mailboxes: create a new mailbox 2020-05-23 16:17:42 +02:00
Son Nguyen Kim
96502c677d
Merge pull request #204 from simple-login/domain-trash
Domain trash
2020-05-23 12:23:02 +02:00
Son NK
56a483d579 re-organize readme 2020-05-23 12:22:26 +02:00
Son NK
6d9d017c08 re-organize README 2020-05-23 12:18:32 +02:00
Son NK
95ae2ec254 Add migrate_domain_trash() to move deleted alias to the correct trash 2020-05-23 12:17:50 +02:00
Son NK
be7ef9bbe9 migration script 2020-05-23 12:06:45 +02:00
Son NK
2fbc2c171b check domain trash when creating custom alias in api 2020-05-23 12:02:01 +02:00
Son NK
40ec9f44a4 check if an alias is domain trash before creating it 2020-05-23 11:51:35 +02:00
Son NK
17acaec214 save deleted alias to domain trash if it belongs to a custom domain, otherwise global trash 2020-05-23 11:49:34 +02:00
Son NK
a3ff19dac4 create custom domain trash page 2020-05-23 11:48:43 +02:00
Son NK
bad3c39921 Add DomainDeletedAlias to store all deleted aliases for a domain 2020-05-23 11:44:15 +02:00
Son NK
1beb7c004b improve fake_data 2020-05-22 14:13:17 +02:00
Son NK
a512fbc6e5 use POSTFIX_PORT instead of 25 if it's set 2020-05-21 20:43:12 +02:00
Son NK
817e4e0f87 add POSTFIX_PORT param 2020-05-21 20:43:12 +02:00
Son Nguyen Kim
57e3e29e70
Merge pull request #200 from SibrenVasse/dkim_fix
Fix reverse alias replacement
2020-05-21 20:08:38 +02:00
Son NK
516485d4d6 warning log for "cancel subscription" 2020-05-21 20:07:41 +02:00
Son NK
6cc8dd548c use warning level for "emails were sent to in the last 24h" error 2020-05-21 20:07:04 +02:00
Son NK
281331bc51 use warning level for "Alias creation time expired" error 2020-05-21 20:05:54 +02:00
Son NK
5ada83d48d use warning level for "Cannot parse original message" error 2020-05-21 20:05:07 +02:00
Son NK
9a423f3247 make should_append_alias case insensitive 2020-05-20 22:35:28 +02:00
Son NK
3898d2d7a6 fix 2020-05-20 22:34:06 +02:00
Son NK
cb036f651d do not call lower() on signed_suffix 2020-05-20 18:23:13 +02:00
Son NK
32a9bd9095 make sure to use lowercase for mailbox email 2020-05-20 18:16:18 +02:00
Son NK
93d972df09 make sure to use lowercase for alias email 2020-05-20 18:12:14 +02:00
Sibren Vasse
1df9f8a95c Do reverse alias replacement before DKIM signing and replace by contact email 2020-05-20 10:56:42 +02:00
Son NK
2217805d8c no need to move alias to global trash when a domain is deleted 2020-05-20 10:16:55 +02:00
devStorm
4fd7bf40ab
use dt filter 2020-05-18 14:05:03 -07:00
devStorm
7bd97e13b0
fido_model -> fidos 2020-05-18 13:55:38 -07:00
devStorm
ea914e0378
Rename FIDO->Fido 2020-05-18 13:54:05 -07:00
devStorm
7d1a744fe2
typo 2020-05-18 13:47:42 -07:00
devStorm
5c3d2c19c8
module level and can be prefixed 2020-05-18 13:46:13 -07:00
devStorm
c0a751ff13
Put button inside the form 2020-05-18 13:45:02 -07:00
Son NK
300ece2440 pycharm format 2020-05-18 19:15:20 +02:00
Son NK
e22f5d1c63 Improve wordings 2020-05-18 19:14:52 +02:00
Son Nguyen Kim
484aca1342
Merge pull request #197 from SibrenVasse/remove-reply-string
Remove reply string
2020-05-18 19:09:43 +02:00
devStorm
e892535287
Black 2020-05-18 05:28:18 -07:00
devStorm
0a59dd5638
Activate key on enter 2020-05-18 05:27:57 -07:00
devStorm
d91fbb563a
Remove debug keys 2020-05-18 05:11:54 -07:00
devStorm
45023cd775
Copywriting consistency 2020-05-18 05:10:19 -07:00
devStorm
b64ed7ad63
key management page 2020-05-18 05:07:06 -07:00
devStorm
6ea17b4feb
remove debug code for sudo 2020-05-18 03:03:14 -07:00
devStorm
6509053fa8
named key 2020-05-18 02:55:41 -07:00
devStorm
0cdd0b3b07
black 2020-05-18 02:15:52 -07:00
devStorm
f79eb90d2a
sudo mode 2020-05-18 02:14:40 -07:00
devStorm
35f0c094fe
black 2020-05-18 01:04:45 -07:00
devStorm
419aa95f1f
more verify 2020-05-18 01:02:58 -07:00
devStorm
ec91d280bb
Verify 2020-05-18 00:08:06 -07:00
devStorm
2b8febe0b9
black 2020-05-18 00:06:24 -07:00
devStorm
9fb91c83e7
more setup 2020-05-18 00:01:27 -07:00
devStorm
f2f6e13af7
DB & Setup ready for multi-keys 2020-05-17 22:05:37 -07:00
Sibren Vasse
e905e151ca Create user setting for replacing reverse alias (default: false) 2020-05-17 19:40:46 +02:00
Son Nguyen Kim
1997c207ed
Merge pull request #196 from SibrenVasse/error_handling
Move api error handling to global error handler
2020-05-17 18:02:23 +02:00
Sibren Vasse
2d7bd225e9 Move api error handling to global error handler 2020-05-17 15:27:24 +02:00
Son NK
b84b7c332e replace the "ra+string@simplelogin.co" by the alias 2020-05-17 14:11:24 +02:00
Son NK
6300c0eaa1 an alias can still be disabled even of original message cannot be parsed. 2020-05-17 14:01:55 +02:00
Son Nguyen Kim
72a8bf7b39
Merge pull request #194 from simple-login/alias-disable-pgp
Alias disable pgp
2020-05-17 12:51:26 +02:00
Son NK
d439871f65 generate new migration script 2020-05-17 12:51:07 +02:00
Son NK
4fe02266f2 Revert "migration sql"
This reverts commit 0524944d2a.
2020-05-17 12:50:39 +02:00
Son NK
12b76dd33b Merge branch 'master' into alias-disable-pgp 2020-05-17 10:46:44 +02:00
Son Nguyen Kim
fb17dca2cf
Merge pull request #195 from simple-login/recovery-code
Recovery code
2020-05-17 10:46:19 +02:00
Son NK
87d52216cb reformat 2020-05-17 10:35:11 +02:00
Son NK
5ff31f3eb4 migration script 2020-05-17 10:34:55 +02:00
Son NK
20e66edbaa fix redirection to next page 2020-05-17 10:28:00 +02:00
Son NK
2e208ed505 display recovery code options on mfa and fido page 2020-05-17 10:27:20 +02:00
Son NK
da4e0bf384 create /auth/recovery page 2020-05-17 10:17:52 +02:00
Son NK
043ecd4fac redirect user to recovery codes page after MFA setup. Remove all recovery codes when user is no more MFA. 2020-05-17 10:11:38 +02:00
Son NK
3f7842ed3e create /recovery_code page 2020-05-17 10:05:55 +02:00
Son NK
aaa1a869ea add RecoveryCode model 2020-05-17 09:59:07 +02:00
Son NK
0524944d2a migration sql 2020-05-16 20:52:14 +02:00
Son NK
f35b9e7542 do not encrypt if alias disables PGP 2020-05-16 20:51:07 +02:00
Son NK
13bb9810b6 use can disable PGP on an alias 2020-05-16 20:51:07 +02:00
Son NK
5e6454e6de use a different class for alias toggle 2020-05-16 20:51:07 +02:00
Son NK
80f614da6c refactor: remove unused var 2020-05-16 20:51:07 +02:00
Son NK
b167297808 Support disable_pgp in update alias endpoint 2020-05-16 20:51:07 +02:00
Son NK
95213b6d85 Add alias.disable_pgp column 2020-05-16 20:51:07 +02:00
Son NK
083a857f1b disable intro_shown in fake data 2020-05-16 20:50:57 +02:00
Son NK
62ed7def00 fix wording 2020-05-16 20:50:27 +02:00
Son NK
82a92dee52 fix migration 2020-05-16 20:44:06 +02:00
Son Nguyen Kim
78ff15912e
Merge pull request #190 from SibrenVasse/sender_format
Add more From: sender formatting options
2020-05-16 20:38:13 +02:00
Sibren Vasse
a5f24e0227 Add more From: sender formatting options 2020-05-16 18:44:31 +02:00
Son NK
f577adc0d4 log more info 2020-05-16 18:24:51 +02:00
Son Nguyen Kim
35ec3f4633
Merge pull request #193 from simple-login/dependabot/npm_and_yarn/static/jquery-3.5.1
Bump jquery from 3.4.1 to 3.5.1 in /static
2020-05-16 12:37:14 +02:00
dependabot[bot]
71136e64d9
Bump jquery from 3.4.1 to 3.5.1 in /static
Bumps [jquery](https://github.com/jquery/jquery) from 3.4.1 to 3.5.1.
- [Release notes](https://github.com/jquery/jquery/releases)
- [Commits](https://github.com/jquery/jquery/compare/3.4.1...3.5.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-05-16 10:21:32 +00:00
Son Nguyen Kim
08b470d2a6
Merge pull request #178 from simple-login/multiple-mailboxes
Multiple mailboxes
2020-05-16 12:20:36 +02:00
Son NK
a4d17e7afc use multiple-select instead of bootstrap-select 2020-05-16 12:17:26 +02:00
Son NK
362d101bab Merge branch 'master' into multiple-mailboxes
# Conflicts:
#	app/dashboard/templates/dashboard/custom_alias.html
#	email_handler.py
#	templates/emails/com/newsletter/mobile-darkmode.html
2020-05-16 11:28:25 +02:00
Son NK
a7a29ab8c9 fix file name 2020-05-15 23:53:17 +02:00
Son NK
c9b75c338e move theme.js to static/ and include it in base.html 2020-05-15 23:40:30 +02:00
Son Nguyen Kim
d97b52184e
Merge pull request #186 from SibrenVasse/dark_flash
Frontend improvements
2020-05-15 23:37:30 +02:00
Son Nguyen Kim
069997ddb9
Merge pull request #192 from simple-login/fix-user-email
Fix user cannot change personal email back and better naming.
2020-05-15 23:24:22 +02:00
Son NK
7ed77a66b2 format 2020-05-15 23:18:42 +02:00
Son NK
2978bfb281 Fix user cannot change personal email back and better naming.
Happens when user
- changes their personal email
- wants to change back: they can't as this email is already used as mailbox
2020-05-15 23:18:30 +02:00
Son Nguyen Kim
ec8f46f01a
Merge pull request #191 from SibrenVasse/spam
Also enable spam check when pgp is enabled
2020-05-15 18:06:57 +02:00
Son NK
6fb85c81fc fix format 2020-05-15 16:50:14 +02:00
Son NK
f04caa3c35 move SQLALCHEMY_ECHO option to create_app(): useful when profiling 2020-05-15 16:48:42 +02:00
Son NK
aba0a534c0 When a mailbox is deleted, only put alias that has this mailbox as single mailbox to global trash 2020-05-15 16:47:55 +02:00
Son NK
ff1aa72b1d lazy load alias._mailboxes and alias.mailbox 2020-05-15 16:46:02 +02:00
Son NK
0b652cf3f8 remove AliasMailbox.user_id column 2020-05-15 16:35:57 +02:00
Sibren Vasse
8769383724 Also enable spam check when pgp is enabled 2020-05-15 16:34:07 +02:00
Son NK
3d4b44dd15 handle the case contact_from_header can be None 2020-05-15 15:46:37 +02:00
Son NK
c8f1244d81 optimize cron job 2020-05-15 15:31:58 +02:00
Son NK
355b4dc2cf remove too verbose log 2020-05-15 15:30:19 +02:00
Sibren Vasse
012bc52694 Fix formatting 2020-05-15 13:42:48 +02:00
Sibren Vasse
c0041d55bc Set input background to white for light theme 2020-05-15 13:42:48 +02:00
Sibren Vasse
e44d92705c Add missing last_page check (index) Move disabled class to correct element (alias_log) 2020-05-15 13:42:48 +02:00
Sibren Vasse
cb269a1bbe Change pagination style 2020-05-15 13:42:48 +02:00
Sibren Vasse
bdc3102420 Fix space error footer 2020-05-15 13:42:48 +02:00
Sibren Vasse
c6e291f7e8 Dark theme: prevent white flash on page load 2020-05-15 13:42:48 +02:00
Son Nguyen Kim
4d87df01a3
Merge pull request #181 from developStorm/webauthn-patch-1
Auto activate WebAuthn authentication
2020-05-14 20:39:49 +02:00
Son Nguyen Kim
50cdbc2b74
Merge pull request #183 from simple-login/api-error
API Error handling for 404 and 500
2020-05-14 20:36:53 +02:00
Son Nguyen Kim
ca366e35b6
Merge pull request #184 from simple-login/empty-from-header
If From header is empty, try creating contact with envelope sender
2020-05-14 20:36:23 +02:00
Son Nguyen Kim
2def79e689
Merge pull request #187 from SibrenVasse/domain_check
On domain check fail, update database
2020-05-14 20:36:02 +02:00
Sibren Vasse
c7530947d3 On domain check fail, update database 2020-05-14 15:05:04 +02:00
Son NK
3a1af9f424 fall back for UnicodeDecodeError 2020-05-14 13:27:04 +02:00
Son NK
ee19957d5d Add 405 error 2020-05-13 23:28:00 +02:00
Son NK
85130e175b fix dark-mode for modal 2020-05-13 23:02:29 +02:00
Son NK
092d934feb improve wording 2020-05-13 22:55:15 +02:00
Son NK
d802615faa improve newsletter wording 2020-05-13 22:46:57 +02:00
Son NK
ec2d912bb8 mobile-darkmode newsletter 2020-05-13 22:43:03 +02:00
Son NK
c43fa65cd4 If From header is empty, try creating contact with envelope sender 2020-05-13 22:35:27 +02:00
Son NK
37271d10ed mobile-darkmode newsletter 2020-05-13 22:08:14 +02:00
Son NK
5c8c741a6a API Error handling for 404 and 500 2020-05-13 22:02:38 +02:00
Son NK
405c5f8a69 Add Sibren to contributor list 2020-05-13 21:52:07 +02:00
Son NK
e6c37cad0b Handle case where data["receipt"]["in_app"] is empty 2020-05-13 21:41:34 +02:00
Son NK
2b71fee712 use warning log level for "No existing AppleSub" error 2020-05-13 21:38:25 +02:00
devStorm
a9967c9a4d
Auto activate WebAuthn authentication 2020-05-11 19:17:51 -07:00
Son NK
5ce2cca63f Add Raymond as contributor 2020-05-11 23:26:37 +02:00
Son NK
2bc2643cc8 use logo in email base template 2020-05-11 23:23:19 +02:00
Son NK
591fee301e prettify dns page 2020-05-11 23:23:08 +02:00
Son NK
70e842789e make pages compatible with dark-theme 2020-05-11 23:22:15 +02:00
Son NK
54ce1dc964 remove unnecessary max_nb_email limit in spf 2020-05-11 14:46:18 +02:00
Son NK
50a105f156 fix mailbox newsletter 2020-05-11 14:44:57 +02:00
Son NK
36a8e311ea Merge branch 'master' into multiple-mailboxes 2020-05-11 10:21:59 +02:00
Son NK
7c55c5c44a return 250 instead of 451 when SPF fails 2020-05-11 10:21:44 +02:00
Son NK
f9daaf9bd2 add .gitattributes file to override linguist 2020-05-11 09:49:44 +02:00
Son NK
0ad296fa69 add necessary migration 2020-05-10 20:09:54 +02:00
Son NK
cbfeee4e28 display list of mailboxes in alias contact manager 2020-05-10 20:09:54 +02:00
Son NK
8f35290a21 fix overflow error when there are several mailboxes 2020-05-10 20:09:54 +02:00
Son NK
bc55b98e12 display mailbox that a bounce affects 2020-05-10 20:09:54 +02:00
Son NK
0d117126db save the mailbox that a bounce affects 2020-05-10 20:09:54 +02:00
Son NK
0f09ef681c Add EmailLog.bounced_mailbox_id 2020-05-10 20:09:54 +02:00
Son NK
5b71b34f9e handle alias unsubscribe 2020-05-10 20:09:54 +02:00
Son NK
336bdb196d Detect unknown mailbox using envelope mail_from 2020-05-10 20:09:54 +02:00
Son NK
33d578c78e parse _MAILBOX_ID_HEADER to handle bounce message 2020-05-10 20:09:54 +02:00
Son NK
8d65175ac5 set mailbox ID in X-SimpleLogin-Mailbox-ID header 2020-05-10 20:09:54 +02:00
Son NK
97e1c334af call strip() on rcpt_to just to be sure 2020-05-10 20:09:54 +02:00
Son NK
4b479defa8 Support alias having multiple mailboxes in forward phase 2020-05-10 20:09:54 +02:00
Son NK
7f6ba313fd add strip() to rcpt_to just in case 2020-05-10 20:09:54 +02:00
Son NK
2755e67c31 simplify code: replace mailbox_email by mailbox.email 2020-05-10 20:09:54 +02:00
Son NK
59036972f1 refactor handle_forward: move the disabled alias case to the beginning 2020-05-10 20:09:54 +02:00
Son NK
b5e7f05bfc allow user sends emails to his alias from his mailbox 2020-05-10 20:09:54 +02:00
Son NK
f59ccd4018 optimize import email_handler 2020-05-10 20:09:54 +02:00
Son NK
e704497b0f make sure prefix is not empty before submitting 2020-05-10 20:09:54 +02:00
Son NK
e52f2ca6de Support multiple mailboxes in custom alias page 2020-05-10 20:09:54 +02:00
Son NK
b375f87d2c User can update multiple mailboxes 2020-05-10 20:09:54 +02:00
Son NK
dafa23c5bf Add fake aliases with multiple mailboxes 2020-05-10 20:09:54 +02:00
Son NK
90dae2e3c8 Support mailbox_ids in PUT /api/aliases/:alias_id 2020-05-10 20:09:54 +02:00
Son NK
165d986561 add mailboxes to GET /api/v2/aliases 2020-05-10 20:09:54 +02:00
Son NK
684e8983ef Add AliasMailbox table 2020-05-10 20:09:54 +02:00
Son NK
6058257509 add bootstrap-select 2020-05-10 20:09:54 +02:00
Son NK
47430725a7 improve doc 2020-05-10 20:09:54 +02:00
Son Nguyen Kim
8535853730
Merge pull request #177 from simple-login/darkmode-2
Dark mode implementation
2020-05-10 20:09:14 +02:00
doanguyen
7ef78c991f Dark mode implementation
* Using CSS variable as a solution
* The new darkmode css is now live in another file so that upgarding
dashboard.css doesn't affect the darkmode itself
* Used a naive darkmode controller by client javascript. No persistance
state is stored in the backend at the moment
2020-05-10 16:52:14 +02:00
Son NK
b90d4037e9 v3.1.0 2020-05-10 14:45:56 +02:00
Son NK
92cd75f14a Add DISABLE_ONBOARDING param 2020-05-10 14:43:41 +02:00
Son NK
cde8452e5b Fix Google oauth_state KeyError 2020-05-10 11:34:32 +02:00
Son NK
8fa0927826 Set SESSION_COOKIE_SAMESITE to Lax 2020-05-10 11:34:23 +02:00
Son NK
b47b74d98a Give more info on the trial period 2020-05-10 10:54:19 +02:00
Son NK
53e04a8066 fix test 2020-05-10 10:42:18 +02:00
Son NK
9ddb8ff2d4 add more info to spf alert email. Set the max number of emails per 24h to 1 2020-05-10 10:37:56 +02:00
Son NK
ac6d1c1106 able to set nb max alert in send_email_with_rate_control 2020-05-10 10:34:39 +02:00
Son NK
8244fa01e7 only show advanced options when spf_available 2020-05-10 09:21:55 +02:00
Son NK
0bfd6b3ec7 remove deleted_alias page 2020-05-10 09:20:08 +02:00
Son NK
526df4ea09 fix DeletedALias 2020-05-09 23:39:57 +02:00
Son Nguyen Kim
c308e9f9bf
Merge pull request #176 from simple-login/spf2
Alert user when SPF fails
2020-05-09 23:16:14 +02:00
Son NK
06c1128ee6 reformat 2020-05-09 23:12:46 +02:00
Son NK
a48f7db599 refactor: create handle_unknown_mailbox() 2020-05-09 23:12:30 +02:00
Son NK
9234527ea6 refactor: put spf handling into a method 2020-05-09 23:09:11 +02:00
Son NK
178515dbde alert user when spf fails 2020-05-09 23:00:30 +02:00
Son NK
ac4e1fab77 email template to alert SPF 2020-05-09 22:58:38 +02:00
Son NK
c6e293ef8e small refactor 2020-05-09 22:54:55 +02:00
Son NK
4e4eda4efa display enforce-SPF option. Change wording. 2020-05-09 22:29:32 +02:00
Son NK
b95b758692 Optimize imports 2020-05-09 20:49:38 +02:00
Son Nguyen Kim
31341ecae7
Merge pull request #175 from simple-login/rate-control
Email Rate control
2020-05-09 20:47:08 +02:00
Son NK
3ebaa54a4c migration script 2020-05-09 20:45:23 +02:00
Son NK
7833d4609f Use send_email_with_rate_control when alerting user 2020-05-09 20:45:04 +02:00
Son NK
d9f1fb9130 Create send_email_with_rate_control(): same as send_email() but with rate control 2020-05-09 20:43:17 +02:00
Son NK
7fdef16f37 add SentAlert model to keep track of alert emails sent to user 2020-05-09 20:40:36 +02:00
Son NK
7bb7b92595 Fix migration 2020-05-09 18:34:34 +02:00
Son Nguyen Kim
b95d815e5c
Merge pull request #168 from simple-login/global-trash
Global trash
2020-05-09 18:08:51 +02:00
Son Nguyen Kim
5fa2a86f23
Merge pull request #170 from developStorm/webauthn-patch
🐛 WebAuthn bug fixes
2020-05-09 18:05:34 +02:00
Son Nguyen Kim
54b32be321
Merge pull request #174 from simple-login/spf-update
Spf update
2020-05-09 18:04:43 +02:00
Son Nguyen Kim
16d8737770
Merge pull request #171 from SibrenVasse/secure_cookie
Session cookie flags.
2020-05-09 18:03:15 +02:00
Son NK
e84e4d50c7 add logging 2020-05-09 17:48:57 +02:00
Son NK
295c2fd03f Hide the SPF toggle 2020-05-09 17:34:59 +02:00
Son NK
0add756654 make sure SPF exception does not stop reply-phase 2020-05-09 17:34:49 +02:00
Son NK
7654992fc2 refactoring: replace "X-SimpleLogin-Client-IP" by constant 2020-05-09 17:31:37 +02:00
Son NK
bd68a52158 make sure to remove "X-SimpleLogin-Client-IP" during forward 2020-05-09 17:30:21 +02:00
Son NK
4cf868e5f1 Fix enforce-spf.md 2020-05-09 17:26:39 +02:00
Son NK
2d8f056e11 Fix wording 2020-05-09 17:26:26 +02:00
Son NK
5ec0ea5f6c Report error when SPF fails on emails sent from mailbox. Return 451 instead of 550 to avoid bounce emails. 2020-05-09 14:52:39 +02:00
Son NK
a3a8a13840 Add enforce-spf doc 2020-05-09 14:52:04 +02:00
Son NK
acf628f8f2 fix migration script 2020-05-09 14:50:53 +02:00
Son Nguyen Kim
e23887bb37
Merge pull request #164 from SibrenVasse/spf
Enforce SPF
2020-05-09 14:37:09 +02:00
Sibren Vasse
001079bdc5 Enforce SPF 2020-05-09 14:15:08 +02:00
Sibren Vasse
e7c3a127b8 Set samesite and secure attributes of session cookie. Enable strong session protection. 2020-05-09 14:13:37 +02:00
Son Nguyen Kim
8417bb5ed8
Merge pull request #169 from simple-login/github-action
Apply github action on PR, upgrade to action v2 and use cache
2020-05-09 09:06:54 +02:00
devStorm
d236f906ad
🐛 WebAuthn bug fixes
- User may not have name
- user_verification should be discouraged to work on iOS
2020-05-08 14:21:38 -07:00
Son NK
225fd4bbb0 name -> repository for docker/build-push-action@v1 2020-05-08 22:30:09 +02:00
Son NK
e68eab44b0 allow user who has TOTP enabled to continue using the mobile app 2020-05-08 20:23:13 +02:00
Son NK
88b957fe8b Apply github action on PR, upgrade to action v2 and use cache 2020-05-08 20:15:57 +02:00
Son NK
2c1daf5bb1 reformat main.yml 2020-05-08 20:14:20 +02:00
Son NK
75a28c53cf fix grammar mistake 2020-05-08 13:40:21 +02:00
Son NK
ad194c46f2 remove lifetime licence in pricing page 2020-05-08 12:38:42 +02:00
Son NK
cf35fe2646 Put all aliases belonging to a domain to global trash when the domain is deleted 2020-05-07 22:50:45 +02:00
Son NK
9898d85722 Put all aliases belonging to a directory to global trash when this directory is deleted 2020-05-07 22:42:39 +02:00
Son NK
7a1f944887 Put all aliases belonging to a mailbox to global trash when this mailbox is deleted 2020-05-07 22:40:53 +02:00
Son NK
0441e5e2a9 Remove DeletedAlias.user_id column 2020-05-07 22:40:30 +02:00
Son NK
5b4eafce50 fix formatting 2020-05-07 22:28:49 +02:00
Son NK
2f5a03dcad make sure to add alias into global trash when deleting it 2020-05-07 22:27:27 +02:00
Son NK
300f1d7032 Override Alias.create to check in global trash first 2020-05-07 22:23:36 +02:00
Son Nguyen Kim
18e50e4a28
Merge pull request #167 from simple-login/disable-social-login-signup
disable sign-up via social login
2020-05-07 22:02:08 +02:00
Son NK
ccb30a2def disable sign-up via social login 2020-05-07 22:01:14 +02:00
Son Nguyen Kim
a785e664e9
Merge pull request #165 from simple-login/same-mailbox-different-user
Same mailbox different user
2020-05-07 21:56:36 +02:00
Son Nguyen Kim
891c06fb15
Merge pull request #166 from simple-login/fido-api
return 403 if user enables FIDO
2020-05-07 21:56:23 +02:00
Son NK
f929f23acc return 403 if user enables FIDO 2020-05-07 21:54:36 +02:00
Son NK
c85ea1538e Allow the same mailbox used by different user 2020-05-07 21:47:11 +02:00
Son NK
f8e896541d replace Mailbox.email unique constraint by (email, user_id) 2020-05-07 21:46:16 +02:00
Son Nguyen Kim
149a06dd68
Merge pull request #163 from simple-login/fix-custom-domain
fix: wrongly set alias custom_domain
2020-05-07 20:49:42 +02:00
Son NK
806f7016ae fix: wrongly set alias custom_domain when custom_domain is in EMAIL_ALIAS 2020-05-07 20:48:11 +02:00
Son Nguyen Kim
f5efab940c
Merge pull request #162 from simple-login/fido-beta
Add Fido as a beta feature
2020-05-07 19:28:54 +02:00
Son NK
101ab408b2 black format 2020-05-07 18:01:12 +02:00
Son NK
985e4ee2f8 sql migration for fido 2020-05-07 17:59:29 +02:00
Son NK
0a497c9f67 put migration generation into a script 2020-05-07 17:59:21 +02:00
Son NK
ef2a385563 redirect user to TOTP in welcome email 2020-05-07 17:58:36 +02:00
Son NK
4709237b92 only user with can_use_fido can use fido 2020-05-07 17:58:24 +02:00
Son NK
18d62a81d1 add User.can_use_fido 2020-05-07 17:56:25 +02:00
Son NK
84c529c867 optimize import 2020-05-07 17:49:29 +02:00
Son NK
fe1262686e black format 2020-05-07 17:48:44 +02:00
Son Nguyen Kim
e35fb631cf
Merge pull request #159 from developStorm/master
Implement WebAuthn
2020-05-07 17:47:06 +02:00
devStorm
2290a90b09
Use try-else 9b8340f3e0 (r421465450) 2020-05-07 05:41:34 -07:00
devStorm
b0c39635a5
Remove credential_id variable 2020-05-07 05:37:03 -07:00
devStorm
e4895b52a0
fix SITE_URL 2020-05-07 05:34:17 -07:00
devStorm
f7e3320242
model - fido_enabled 2020-05-07 05:32:52 -07:00
devStorm
9b8340f3e0
Black formatted 2020-05-07 02:53:28 -07:00
devStorm
0052dad13e
Do not show full error msg to user 2020-05-07 02:48:56 -07:00
devStorm
282cbe25a3
Calculate RP_ID in config 2020-05-07 02:39:30 -07:00
devStorm
c38b3c768c
fix SimpleLogin brand name 2020-05-07 02:34:19 -07:00
devStorm
3ab3f819b7
Make RP_ID a constant 2020-05-07 02:33:24 -07:00
devStorm
b8b1313db9
typo 'infomation' 2020-05-07 02:31:42 -07:00
Son NK
16de59a9f5 mailbox can be other user's email 2020-05-06 12:34:52 +02:00
Son NK
35b1972730 increase the expired alias creation session to 600 secs 2020-05-06 10:10:47 +02:00
Son NK
d9e9a54082 add more info in "alias expire" error 2020-05-06 10:09:17 +02:00
Son NK
e9d03d1d4b Handle the case where a deleted mailbox verification link is clicked 2020-05-06 10:06:05 +02:00
devStorm
ced02a8f20
remove debug code 2020-05-05 14:26:26 -07:00
devStorm
fc001cfc24
fix exception handling 2020-05-05 14:13:01 -07:00
devStorm
1d24b6da08
License for base64.js 2020-05-05 05:37:57 -07:00
devStorm
370b71ebd3
Setting page options 2020-05-05 05:28:27 -07:00
devStorm
9da6054ec0
Allow to use either OTP or FIDO for 2FA 2020-05-05 05:16:33 -07:00
devStorm
650d6e35f0
FIDO login middleware 2020-05-05 05:03:29 -07:00
Son NK
103418dff7 Strip off http:// or https:// prefix in domain 2020-05-05 12:46:32 +02:00
Son NK
3eb904c882 set max length for domain 2020-05-05 12:46:11 +02:00
Son NK
7c31d39919 Add how to generate migration script 2020-05-05 12:39:31 +02:00
devStorm
9b976efa50
Merge branch 'master' of github.com:developStorm/simple-login 2020-05-05 03:16:58 -07:00
devStorm
286b1143ca
Store sign count 2020-05-05 03:16:52 -07:00
devStorm
334cc98038
Store sign count 2020-05-05 03:16:09 -07:00
devStorm
705941b8b8
Unlink security key 2020-05-05 02:20:52 -07:00
devStorm
a32b69078f
Key registration (Backend) 2020-05-05 01:58:42 -07:00
devStorm
3ce4dfb371
Security key setup page (front-end) 2020-05-05 01:32:49 -07:00
devStorm
117b120556
Add python dependency webauthn 2020-05-05 01:31:19 -07:00
Son Nguyen Kim
042a421c2c
Merge pull request #154 from SibrenVasse/master
Add default alias name to custom domain
2020-05-03 19:51:43 +02:00
Sibren Vasse
0e4799030d Add default alias name to custom domain 2020-05-03 19:35:02 +02:00
Son NK
4ca6b02047 fix DKIM cname check 2020-05-03 12:48:42 +02:00
Son Nguyen Kim
9991723f5e
Merge pull request #153 from simple-login/dns
Add DMARC, improve DNS setup
2020-05-03 12:03:34 +02:00
Son NK
5b8c7884b4 sql migration 2020-05-03 12:02:34 +02:00
Son NK
753e82d490 Add DMARC 2020-05-03 12:01:31 +02:00
Son NK
a270987f70 Add CustomDomain.dmarc_verified column 2020-05-03 11:51:22 +02:00
Son NK
6a42673229 remove the copy button, use CNAME for DKIM 2020-05-03 11:19:14 +02:00
Son Nguyen Kim
ba4d7c3ee6
Merge pull request #152 from simple-login/prettify
Prettify: Make api key, custom domain, directory, mailboxes more compact
2020-05-03 11:01:26 +02:00
Son NK
625def2367 use 2-column layout for api key, domain, directory, mailbox 2020-05-03 10:48:21 +02:00
Son NK
8a147e36a7 create how-to-use section for api key, domain, directory, mailbox 2020-05-03 10:47:29 +02:00
Son NK
fe73005d49 fix referral display on mobile 2020-05-03 10:46:35 +02:00
Son Nguyen Kim
b5120e78d6
Merge pull request #149 from simple-login/referral-name
Referral improvements
2020-05-02 18:15:17 +02:00
Son NK
249eab31e7 sql migration 2020-05-02 18:14:17 +02:00
Son NK
eff0eb9e32 can delete referral 2020-05-02 18:14:09 +02:00
Son NK
1667356742 User can update/create referral name 2020-05-02 18:11:10 +02:00
Son NK
3ce3a05c7b Add referral name 2020-05-02 18:08:05 +02:00
Son Nguyen Kim
7ceb9440de
Merge pull request #148 from simple-login/anti-tamper
Anti alias suffix tamper
2020-05-02 16:32:43 +02:00
Son NK
c35fbf9797 fix test_encode_decode 2020-05-02 16:26:50 +02:00
Son NK
62bec84900 Add obsolete warnings 2020-05-02 16:23:40 +02:00
Son NK
d32669f515 Add /api/v2/alias/custom/new 2020-05-02 16:22:17 +02:00
Son NK
72e9b52b29 Add /api/v4/alias/options 2020-05-02 16:21:18 +02:00
Son NK
56967e7a38 Add oauth_tester to test oauth 2020-05-02 12:50:37 +02:00
Son NK
abeb246b2c add alias suffix anti-tampering to oauth authorize 2020-05-02 12:50:19 +02:00
Son NK
9874422700 refactor custom_alias: create available_suffixes() 2020-05-02 12:34:11 +02:00
Son NK
c7ebee2118 Fix suggested_emails: only return enabled aliases 2020-05-02 12:28:44 +02:00
Son NK
8467d2b934 fix <select>: add "form-control" class on all <select> 2020-05-02 12:28:20 +02:00
Son NK
0edcc25289 refactor verify_prefix_suffix: remove user_custom_domains param 2020-05-02 12:27:54 +02:00
Son NK
db92003e5f Anti tamper: avoid submitting any suffix 2020-05-02 12:15:03 +02:00
Son NK
5e174b08f4 Add Android app to browser-extension email 2020-05-02 11:00:03 +02:00
Son NK
9a4df685da remove referral section from Settings 2020-05-01 18:32:35 +02:00
Son NK
95a90a9979 use log warning for verifyReceipt 2020-05-01 18:31:06 +02:00
Son Nguyen Kim
061e5aec4e
Merge pull request #147 from simple-login/replace-notie
Replace notie by bootbox
2020-04-30 23:08:58 +02:00
Son NK
ef58b935d4 remove unused code 2020-04-30 22:39:31 +02:00
Son NK
2df6b8023d remove notie 2020-04-30 22:39:23 +02:00
Son NK
7277c30735 use bootbox instead of notie 2020-04-30 22:37:39 +02:00
Son NK
1be3297551 install bootbox 2020-04-30 22:35:51 +02:00
Son NK
9be813b96d fix naming 2020-04-29 16:57:28 +02:00
Son Nguyen Kim
c4e20c2f9c
Merge pull request #145 from simple-login/macapp
Support Macapp payment
2020-04-29 16:29:13 +02:00
Son NK
8ae51998f5 take into account MacApp 2020-04-29 15:50:06 +02:00
Son NK
9c60cd3d88 Add MACAPP_APPLE_API_SECRET param 2020-04-29 15:47:03 +02:00
Son NK
b750c6e011 prettify some pages 2020-04-28 20:25:40 +02:00
Son NK
0bb311464c remove custom-select as it's not displayed properly 2020-04-28 20:23:24 +02:00
Son NK
cc6293d698 Order mailbox by created order 2020-04-28 20:22:37 +02:00
Son NK
d46e8e52a4 Order directory by created order 2020-04-28 20:09:57 +02:00
Son NK
d453c83974 order api key by created order 2020-04-28 20:08:45 +02:00
Son NK
e1d8c55a66 add mention of MyDigiPassword to the 2FA app list 2020-04-28 19:52:18 +02:00
Son NK
ca2b177e02 fix test: use valid domain 2020-04-27 23:15:30 +02:00
Son Nguyen Kim
04f1cd5f7b
Merge pull request #144 from simple-login/clean-up-ui
Deprecate social login
2020-04-27 23:10:33 +02:00
Son NK
6acbf2f8dc do not accept email without MX record 2020-04-27 23:08:34 +02:00
Son NK
96366ddcfa Deprecate social login, prettify some pages 2020-04-27 23:08:21 +02:00
Son NK
a069fe7b6a do not return error when user doesn't exist on forgot_password 2020-04-27 22:57:55 +02:00
Son NK
26a094469b remove logout.html 2020-04-27 22:56:44 +02:00
Son Nguyen Kim
af9e93ea30
Merge pull request #143 from simple-login/check-mx
Check MX record of email domain to see if it is disposed
2020-04-27 20:54:28 +02:00
Son NK
96d93c824a fix test 2020-04-27 20:22:25 +02:00
Son NK
fd90811e85 Prettify alias contact manager 2020-04-27 19:58:55 +02:00
Son NK
ba081a597a Fix: do not use spam check on disabled alias 2020-04-27 18:18:40 +02:00
Son NK
acacab887e Check MX record of email domain to see if it is disposed 2020-04-27 18:17:50 +02:00
Son Nguyen Kim
ed16e9b478
Merge pull request #142 from simple-login/fix-navigation
Fix navigation and change general stats
2020-04-27 09:30:39 +02:00
Son NK
01cc07b9fe change the general stats: display #alias, forward, reply, block 2020-04-27 09:30:08 +02:00
Son NK
23c17b8cff Fix filker reset when going to next page 2020-04-27 09:29:27 +02:00
Son Nguyen Kim
f8ba0d954f
Merge pull request #141 from simple-login/dashboard-ui
Better dashboard ui, global stats
2020-04-26 18:54:33 +02:00
Son NK
131a0473fd Move alias activity details into collapsed section 2020-04-26 18:53:00 +02:00
Son NK
5bcc2138cf Fix wording in menu: use plurial for consistency 2020-04-26 18:49:46 +02:00
Son NK
76b4611bc2 Show global stats 2020-04-26 18:49:23 +02:00
Son NK
c350bca488 collapsible filters 2020-04-26 18:24:43 +02:00
Son NK
18d1b59845 add vuejs to package.json 2020-04-26 17:51:47 +02:00
Son Nguyen Kim
ee315cfab8
Merge pull request #140 from simple-login/add-ios-app
Add iOS app link to browser-extension email
2020-04-26 16:32:09 +02:00
Son NK
9cd41fbae2 Add iOS app link to browser-extension email 2020-04-26 13:16:49 +02:00
Son Nguyen Kim
d554c9e9e8
Merge pull request #139 from simple-login/sort
More options for sorting and filtering aliases
2020-04-26 13:12:01 +02:00
Son NK
7b2e4da87f Support alias filter 2020-04-26 13:04:27 +02:00
Son NK
12714ae601 Add Only enabled alias sorting option 2020-04-26 12:31:10 +02:00
Son NK
4c64393df1 Add other sorting options: A-Z, Z-A, new-old, old-new 2020-04-26 12:25:12 +02:00
Son Nguyen Kim
5c48f82f41
Merge pull request #138 from simple-login/alias-name
Alias name
2020-04-26 11:08:08 +02:00
Son NK
3063fee472 Use alias name in reply phase 2020-04-26 10:41:24 +02:00
Son NK
7021fd650b User can update alias name 2020-04-26 10:41:08 +02:00
Son NK
63af3297f7 return alias name in GET /api/v2/aliases 2020-04-26 10:38:58 +02:00
Son NK
7a2fc007e8 Accept "name" in PUT /api/aliases/:alias_id 2020-04-26 10:38:16 +02:00
Son NK
22bfb4082e add alias.name column 2020-04-26 10:37:40 +02:00
Son NK
04713eff3d show alias creation date in "more" section if not shown yet 2020-04-26 10:02:10 +02:00
Son Nguyen Kim
6dd3e74c63
Merge pull request #137 from simple-login/mailbox-api
Mailbox api
2020-04-26 00:36:29 +02:00
Son NK
182f01f775 More diverse fake data 2020-04-25 23:42:56 +02:00
Son NK
a422f33323 Add GET /api/mailboxes 2020-04-25 23:42:56 +02:00
Son NK
88ddca54c1 return mailbox in GET /api/v2/aliases 2020-04-25 23:42:56 +02:00
Son Nguyen Kim
c8ed1437f1
Merge pull request #136 from simple-login/refactor
Refactor: remove unused POST
2020-04-25 15:33:18 +02:00
Son NK
ee7e783f2a keep sort param when redirecting back to index 2020-04-25 15:31:20 +02:00
Son NK
a2666cc4fe remove "set-mailbox" form post 2020-04-25 15:30:19 +02:00
Son NK
1a081f87c4 Remove "set-note" post form 2020-04-25 15:28:25 +02:00
Son NK
fb9dc23529 remove form POST switch-email-forwarding 2020-04-25 15:26:07 +02:00
Son NK
bbcd4fc355 remove "trigger-email" 2020-04-25 15:21:35 +02:00
Son NK
af431c3d8b Handle alias can be None when deleting 2020-04-25 15:16:46 +02:00
Son Nguyen Kim
51676f02b5
Merge pull request #135 from simple-login/alias-pagination
Alias pagination, support sorting
2020-04-25 14:02:50 +02:00
Son NK
97544ac760 Update mailbox using ajax
refactor: return Mailbox in User.mailboxes()
2020-04-25 13:49:40 +02:00
Son NK
489153a750 Support update mailbox_id in PUT /aliases/:aliasID 2020-04-25 13:49:40 +02:00
Son NK
203eba9917 use Ajax for save note 2020-04-25 13:49:39 +02:00
Son NK
36aee86590 support sorting: Oldest Alias to Newest, 2020-04-25 13:49:39 +02:00
Son NK
ca6350cc27 optimize import in all files 2020-04-25 13:49:39 +02:00
Son NK
95b71435f9 refactoring: use get_alias_infos_with_pagination_v2 2020-04-25 13:49:39 +02:00
Son NK
72f3e47c3c remove show_intro_test_send_email, highlight from AliasInfo 2020-04-25 13:49:39 +02:00
Son NK
1c9d953044 add mailbox to AliasInfo 2020-04-25 13:49:39 +02:00
Son NK
5839c637f6 use pagination for alias 2020-04-25 13:49:39 +02:00
Son NK
4727249958 do not use alias_info.highlight 2020-04-25 13:49:39 +02:00
Son NK
6c1b39bc04 remove AliasInfo.latest_activity 2020-04-25 13:49:39 +02:00
Son Nguyen Kim
560419c6ad
Merge pull request #134 from simple-login/collapsible
Make alias layout collapsible
2020-04-25 13:49:04 +02:00
Son NK
0c73a36773 disable/enable the send-email button when alias is enabled/disabled 2020-04-25 13:43:32 +02:00
Son NK
ad7d8741f6 reduce top space in default template 2020-04-25 13:43:15 +02:00
Son NK
187d8c0ef2 put alias button to left, search to right 2020-04-25 13:43:14 +02:00
Son NK
8ee34d9132 add more alias in fake_data 2020-04-25 13:42:53 +02:00
Son NK
f34b9f6ca6 Prettify alias page: use collapsible layout 2020-04-25 13:42:53 +02:00
Son NK
c61213fae9 use ajax to switch on/off alias 2020-04-25 13:42:53 +02:00
Son NK
48202e905f rename verify_api_key -> require_api_auth 2020-04-25 13:42:53 +02:00
Son NK
78e94da08c support user already authenticated in verify_api_key 2020-04-25 13:42:53 +02:00
Son NK
ae353dbb25 Use 4 borders for highlight-row (instead of 1) 2020-04-24 09:49:19 +02:00
Son NK
7d35baddd4 do not ask for confirmation when enable/disable alias 2020-04-24 09:47:25 +02:00
Son NK
ba105f076e Fix "new" badge appear on alias modification 2020-04-24 09:45:49 +02:00
Son NK
fc4572e9ba make logo a bit smaller 2020-04-24 09:43:26 +02:00
Son NK
6a67f7946f fix facebook might not return email 2020-04-24 09:17:21 +02:00
Son NK
0f71eff531 handle the case some email providers might strip off the = suffix 2020-04-24 09:09:11 +02:00
Son NK
618d308c22 improve cron stats job 2020-04-23 22:11:43 +02:00
Son NK
734b104c27 remove text on registration waiting page 2020-04-23 22:10:14 +02:00
Son NK
7fff8f84d8 add more debug log 2020-04-21 20:17:43 +02:00
Son NK
318b47af36 Handle the case invalid input for /api/apple/update_notification 2020-04-21 15:54:43 +02:00
Son NK
e50b0d5da5 implement apple_update_notification 2020-04-21 09:34:16 +02:00
Son NK
15219f7021 Support Apple grace period 2020-04-20 23:31:25 +02:00
Son NK
04e7cc448e fix blocked -> block 2020-04-20 19:58:10 +02:00
Son NK
840f827b45 Handle the case "Restore Purchase" on another account 2020-04-19 23:13:43 +02:00
Son NK
b5b4fe2773 make sure original_transaction_id is unique 2020-04-19 23:13:07 +02:00
Son NK
d5e868e629 Fix apple payment 2020-04-19 22:54:21 +02:00
Son NK
34635bf854 use Log.error to know when /api/apple/update_notification is called 2020-04-19 16:06:37 +02:00
Son NK
bca1e227c7 Add /apple/update_notification to test Apple notif 2020-04-19 11:50:29 +02:00
Son NK
1805980cb3 fix planenum enum already used 2020-04-19 11:47:25 +02:00
Son Nguyen Kim
18e5dffcd7
Merge pull request #132 from simple-login/apple
Apple Subscription
2020-04-19 11:22:28 +02:00
Son NK
bf55ba0521 Add output doc for POST /apple/process_payment 2020-04-19 11:22:05 +02:00
Son NK
b33ec7d025 fix reformatting 2020-04-19 11:20:44 +02:00
Son NK
11772d35e1 Add sql migration 2020-04-19 11:18:42 +02:00
Son NK
71d53d16da add poll_apple_subscription(), call it everyday 2020-04-19 11:18:27 +02:00
Son NK
1bba38edb6 Add POST /apple/process_payment 2020-04-19 11:13:38 +02:00
Son NK
85fd4412ba take into account AppleSubscription in premium formula 2020-04-19 10:58:32 +02:00
Son NK
2a837f9213 remove user.is_cancel() 2020-04-19 10:54:15 +02:00
Son NK
f7f1e7f358 replace user.next_bill_date() by sub.next_bill_date.strftime("%Y-%m-%d") 2020-04-19 10:54:05 +02:00
Son NK
b0118e615a Add AppleSubscription model 2020-04-18 20:47:33 +02:00
Son NK
7b965e4121 Add APPLE_API_SECRET param 2020-04-18 20:47:11 +02:00
Son NK
88bf608cf4 no need to bind 7777 port for init_app 2020-04-18 11:56:27 +02:00
Son NK
cf016caa91 Refuse disposable emails in can_be_used_as_personal_email() 2020-04-16 09:43:14 +02:00
Son NK
6fa46042dc Add DISPOSABLE_FILE_PATH param 2020-04-16 09:42:34 +02:00
Son NK
746cd2eb66 Use FIRST_ALIAS_DOMAIN in directory and custom alias 2020-04-15 22:52:30 +02:00
Son NK
28101612db Use FIRST_ALIAS_DOMAIN to create first alias 2020-04-15 22:51:18 +02:00
Son NK
8ebc26f4e7 add FIRST_ALIAS_DOMAIN param 2020-04-15 22:36:50 +02:00
Son NK
a434413304 Add terms and condition mention in register page 2020-04-15 22:32:12 +02:00
Son NK
c17ecba202 add more logging 2020-04-15 21:34:22 +02:00
Son Nguyen Kim
b4211dba78
Merge pull request #131 from simple-login/fix-email
make sure to strip and lower email in input
2020-04-15 21:14:48 +02:00
Son NK
3c9e6fc991 make sure to strip and lower email in input 2020-04-15 21:12:45 +02:00
Son NK
5f784d683a Prettify 2020-04-15 09:16:42 +02:00
Son NK
6c283bc08e Improve doc 2020-04-15 09:01:52 +02:00
Son NK
29c9295e01 set File.user_id to nullable to correspond to existing installation 2020-04-15 09:00:23 +02:00
Son NK
63484c34ca Add migration script to run when upgrading to 3x version 2020-04-15 08:59:37 +02:00
Son NK
298b632c61 Use 3.0.1 instead of 3.0.0 in doc 2020-04-14 22:54:57 +02:00
Son NK
db23867d93 To upgrade to 3x from 2x, user needs to upgrade to 2.1.2 first 2020-04-14 22:54:42 +02:00
Son NK
598f028238 v3.0.1 2020-04-14 22:43:12 +02:00
Son NK
1410d5617f Fix code compatibility with 2x version 2020-04-14 22:42:20 +02:00
Son NK
75f43eefdc replace 2.1.0 by 3.0.0 in upgrade.md 2020-04-14 21:51:58 +02:00
Son NK
703d9385b5 Fix "Content-Transfer-Encoding" issue when encrypting emails 2020-04-14 20:49:48 +02:00
Son NK
6cfd534192 Add LOAD_PGP_EMAIL_HANDLER param. Load PGP keys if LOAD_PGP_EMAIL_HANDLER is set 2020-04-14 12:46:12 +02:00
Son NK
decda875db Add sleep() when sending batch email 2020-04-13 21:42:55 +02:00
Son NK
ed76a8ae8d reformat 2020-04-13 20:51:29 +02:00
Son NK
47f8e6f8e8 Add migration script 2020-04-13 20:51:00 +02:00
Son NK
b838157ad5 User who has lifetime licence or giveaway manual subscriptions can decide to upgrade to a paid plan 2020-04-13 20:50:48 +02:00
Son NK
260ded14ea rename should_upgrade -> should_show_upgrade_button 2020-04-13 20:49:35 +02:00
Son NK
7beae4d846 Add ManualSubscription.is_giveaway column 2020-04-13 20:48:47 +02:00
Son NK
6a617ceeea Add custom error code for 550 SL error 2020-04-13 19:33:45 +02:00
Son Nguyen Kim
82c2c669c2
Merge pull request #129 from simple-login/intro-shown-once
Intro shown once
2020-04-13 13:24:00 +02:00
Son NK
3d10fab3a6 Make sure to show intro to user only once 2020-04-13 13:23:17 +02:00
Son NK
dee6d4959d Add User.intro_shown column 2020-04-13 13:22:52 +02:00
Son NK
18b7c7d495 v3.0.0 2020-04-13 10:48:35 +02:00
Son NK
2a0004c6cf Fix upgrade sl-init command: remove "restart" option for sl-init 2020-04-13 10:08:15 +02:00
Son NK
096a925460 Replace 2.0.0 by 2.1.0 in README 2020-04-13 10:05:52 +02:00
Son NK
6f59e7ea37 Update pricing page 2020-04-12 20:15:02 +02:00
Son NK
e44860329b Make sure user cannot create more than 50 directories 2020-04-12 20:14:49 +02:00
Son Nguyen Kim
b4f28a5156
Merge pull request #126 from simple-login/change-plan
User can change plan
2020-04-12 19:45:59 +02:00
Son NK
70ce48cd79 Disable trial on fake data 2020-04-12 19:43:55 +02:00
Son NK
b041591133 Prettify Settings 2020-04-12 19:43:46 +02:00
Son NK
b845e2a8eb Handle case where subscription_payment_succeeded arrives BEFORE subscription_created 2020-04-12 19:43:35 +02:00
Son NK
9b91f4a4a4 support changing plan 2020-04-12 19:43:07 +02:00
Son NK
51eb550751 fix not using lifetime_or_active_subscription 2020-04-12 19:39:47 +02:00
Son NK
1bed525231 Prettify alert-primary, alert-danger 2020-04-12 19:39:31 +02:00
Son NK
076d9899ea rename 2020-04-12 19:27:14 +02:00
Son NK
70c294bee0 Remove lifetime licence mention for students/professors/... 2020-04-12 11:59:23 +02:00
Son NK
6e1ac4b0e8 Add copy to clipboard in DNS setting 2020-04-11 20:01:47 +02:00
Son NK
a0cdf3ae95 allow user having manual sub or canceled sub to upgrade to lifetime 2020-04-11 10:47:32 +02:00
Son NK
89c41f972c Add id to settings section 2020-04-09 23:01:29 +02:00
Son NK
c3d63f155f Remove font bold from highlight-row 2020-04-09 22:39:53 +02:00
Son NK
bb2476203f User can create referrals 2020-04-09 22:39:39 +02:00
Son NK
8fc88b8253 Set referral when creating User 2020-04-09 22:22:26 +02:00
Son NK
cdf23d04fc Add Referral model 2020-04-09 22:20:06 +02:00
Son NK
a54ac0b3da Add LANDING_PAGE_URL param 2020-04-09 22:18:03 +02:00
Son NK
4789e439db rename auth_login to auth 2020-04-09 20:31:53 +02:00
Son NK
f34ef8781c Do not hardcode MAX_NB_EMAIL_FREE_PLAN in email templates 2020-04-08 23:08:34 +02:00
Son NK
6a19390e3f Create text() macro, no more variable concatenation as render_text()! 2020-04-08 23:07:38 +02:00
Son NK
a548c84694 Make MAX_NB_EMAIL_FREE_PLAN available in all email templates 2020-04-08 23:06:56 +02:00
Son NK
d9d22a56e2 Update to 15 aliases in free plan 2020-04-08 22:38:25 +02:00
Son NK
3f84b9e901 no need to set X-Frame-Options header
as already set by Nginx
2020-04-06 22:36:35 +02:00
Son Nguyen Kim
cb6a18b1bf
Merge pull request #123 from simple-login/get-alias-v2
Add GET /api/v2/aliases
2020-04-06 22:31:31 +02:00
Son NK
5d0519ed86 Add GET /api/v2/aliases 2020-04-06 22:26:35 +02:00
Son NK
7ed317e334 Show alias creation date when no activity 2020-04-05 20:05:49 +02:00
Son NK
cfdaf659f9 Make UI more consistent 2020-04-05 19:59:48 +02:00
Son NK
b19dfc6ef4 Use forward/reply/blocked icon on alias page 2020-04-05 19:42:30 +02:00
Son NK
e83f11342d prettify alias activity page 2020-04-05 19:28:57 +02:00
Son NK
460c306712 Order alias by latest activity instead of alias creation date. Show the latest activity & contact 2020-04-05 19:03:17 +02:00
Son NK
af9178e216 Use non-beta logo 2020-04-05 18:58:22 +02:00
Son NK
bf0f8a913a remove AliasInfo.id 2020-04-05 16:57:47 +02:00
Son NK
b0c3634e72 use dataclass for AliasInfo, remove note 2020-04-05 16:57:28 +02:00
Son NK
67d6ce1cea rename 2020-04-05 16:33:43 +02:00
Son NK
425fdc66c6 Move methods to api/serializer.py 2020-04-05 16:32:38 +02:00
Son NK
f15488f96b refactor: move get_alias_infos_with_pagination, get_alias_info to alias.py 2020-04-05 16:21:13 +02:00
Son NK
f3244eb274 force convert contact_from_header to string 2020-04-05 15:42:09 +02:00
Son NK
2619333cc6 remove uses of website_from 2020-04-05 15:39:48 +02:00
Son NK
18844b7011 use full email header in replace_header_when_reply 2020-04-05 15:27:35 +02:00
Son NK
f78e790b71 set contact name in get_or_create_contact, use contact.new_addrs in replace_header_when_forward 2020-04-05 15:24:09 +02:00
Son NK
afceabeef5 add Contact.new_addr() 2020-04-05 15:21:04 +02:00
Son NK
b01533e9ac Update contact name in forward phase 2020-04-05 14:50:12 +02:00
Son NK
a7eefe8232 rename 2020-04-05 12:59:36 +02:00
Son NK
d1e5b9f9b7 set contact name, use website_email instead of website_from in alias endpoint 2020-04-05 12:58:06 +02:00
Son NK
c686767d4d Fix parseaddr_unicode: take into account email only case 2020-04-05 12:56:17 +02:00
Son NK
6c68b3cda7 Fix website_send_to: prefer using name instead of website_from 2020-04-05 12:48:59 +02:00
Son NK
fbcac59c7f Set contact name when adding contact via contact manager 2020-04-05 12:29:00 +02:00
Son NK
9c22bf479a rename 2020-04-05 12:19:37 +02:00
Son NK
fa8e0aee45 Add contact.name column 2020-04-05 12:18:18 +02:00
Son NK
5fff1e86ce Add parseaddr_unicode() 2020-04-05 12:07:40 +02:00
Son NK
6258ef0c11 Only display website_email instead of website_from in contact manager, alias log and refused emails 2020-04-05 12:02:08 +02:00
Son NK
3a845af2fa use website_email instead of website_from in spam-email 2020-04-05 12:00:01 +02:00
Son NK
a081298756 use website_email instead of website_from in bounce-email 2020-04-05 11:59:24 +02:00
Son NK
b2f22db9f6 use website_email instead of website_from for automatic-disable-alias email 2020-04-05 11:58:13 +02:00
Son NK
5ce21bfa16 Ignore adhoc_* files 2020-04-05 11:56:36 +02:00
Son NK
c163bdc14f use warning for grey listing 2020-04-04 21:59:42 +02:00
Son NK
0657f3dbc2 rename 2020-04-04 20:06:35 +02:00
Son NK
3baddc9206 rename 2020-04-04 20:04:42 +02:00
Son NK
f7e5ab1e34 reaname 2020-04-04 19:21:31 +02:00
Son NK
4d8040c80d Add reverse_alias to GET /api/aliases/:alias_id/activities 2020-04-04 19:18:07 +02:00
Son NK
1c5d6e3299 rename 2020-04-04 19:11:10 +02:00
Son NK
eab45beef2 Use error level when greylisting happens 2020-04-04 18:07:22 +02:00
Son NK
655090242e Fix greylog query 2020-04-04 18:07:05 +02:00
Son NK
eccc8a71e9 use MAX_ACTIVITY_DURING_MINUTE instead of MIN_TIME_BETWEEN_ACTIVITY 2020-04-04 17:22:27 +02:00
Son NK
8caebc0142 Return 421 when there's too much activity on an alias or mailbox 2020-04-04 16:27:22 +02:00
Son NK
661547ec3a refactor: avoid calling "with app.app_context()" all the time 2020-04-04 16:09:24 +02:00
Son NK
c1f5c07d86 Move alias auto-creation to alias_utils 2020-04-04 15:24:27 +02:00
Son NK
0c2bce6931 Add PGP Encryption to pricing page 2020-04-03 23:39:27 +02:00
Son NK
27d048f70b add send-from-alias-from-unknown-sender template in html 2020-04-03 23:39:07 +02:00
Son NK
58ca77e2ae Add browser extension onboarding email 2020-04-02 23:26:17 +02:00
Son NK
85d36f2eac Improve welcome email 2020-04-02 23:24:52 +02:00
Son NK
518cb84677 Use a new favicon 2020-04-02 21:30:36 +02:00
Son NK
3550447a66 use BytesIO as input when encrypting 2020-04-02 21:30:36 +02:00
Son NK
ed8caa237a use email.message_from_bytes instead of Parser(policy=SMTPUTF8).parsestr 2020-04-02 21:30:36 +02:00
Son NK
55b1ce2067 use a different s3 folder for spams 2020-04-02 21:30:36 +02:00
Son NK
55190ff358 use msg.as_bytes() instead of msg.as_string().encode() 2020-04-02 21:30:36 +02:00
Son NK
2041b0aabd Handle the case where path can be None in cron 2020-04-02 21:30:36 +02:00
Son NK
5ff8ae00e5 only lower the email part when creating new contact 2020-04-02 21:30:36 +02:00
Son NK
0517fcfd48 Remove the X-Sender during the reply phase 2020-04-01 20:33:27 +02:00
Son NK
c8ba6e8013 Special handling for self-forward case 2020-04-01 20:32:26 +02:00
Son NK
e9208810af Return user to login page in case of 401 2020-04-01 20:32:08 +02:00
Son NK
7f85ec30bd Improve contact manager: lowercase the contact address before adding 2020-04-01 20:31:47 +02:00
Son NK
62e028c30f Remove the workaround 2020-03-31 22:19:15 +02:00
Son NK
f34c1f555f quick workaround for prod 2020-03-30 22:37:41 +02:00
Son NK
3925ebce1e reformat 2020-03-30 22:12:35 +02:00
Son NK
126aa8824c Add nb bounced and spam to stats 2020-03-30 22:11:19 +02:00
Son NK
bb12b35d2c delete "Received" header in reply phase 2020-03-30 22:05:51 +02:00
Son NK
9500cc6cee Take into account spamassassin spam report 2020-03-30 22:05:31 +02:00
Son NK
33a80236d3 Handle the case the msg is sent from the mailbox to alias. Happen when reply-all 2020-03-30 22:02:22 +02:00
Son NK
ca5e3ac477 Refactoring: better naming 2020-03-30 21:46:52 +02:00
Son NK
917009a803 Improve bounced email wording 2020-03-30 21:45:43 +02:00
Son NK
019f5307c9 Always replace To and Cc header in reply phase 2020-03-30 21:45:18 +02:00
Son NK
9563b706f2 Add EmailLog is_spam, spam_status column 2020-03-30 21:42:25 +02:00
Son NK
7c0e4b369a fix contact not highlighted after creation 2020-03-30 21:41:18 +02:00
Son NK
59a336f5cd use warning level for bounce log message 2020-03-29 23:13:12 +02:00
Son NK
4c1c02db60 Add /v3/alias/options 2020-03-29 23:13:04 +02:00
Son Nguyen Kim
64eed21cf0
Merge pull request #122 from simple-login/unsubscribe
Support one-click unsubscribe
2020-03-28 23:22:27 +01:00
Son NK
da6441b4b8 Handle on-click unsubcribe 2020-03-28 23:19:25 +01:00
Son NK
88d63bd931 add UNSUBSCRIBER config 2020-03-28 23:15:45 +01:00
Son Nguyen Kim
00313ffdf4
Merge pull request #121 from simple-login/sender-format
User can choose sender format
2020-03-28 22:58:09 +01:00
Son NK
7f49312255 use user preferred sender format 2020-03-28 22:37:00 +01:00
Son NK
33fd40f6ce User can choose which sender format they prefer 2020-03-28 22:36:19 +01:00
Son NK
65ca7d2a71 add User.use_via_format_for_sender column 2020-03-28 22:35:29 +01:00
Son Nguyen Kim
09fd21eda7
Merge pull request #120 from simple-login/cc
Handle CC and multiple recipients
2020-03-28 21:48:40 +01:00
Son NK
5771eaeb63 Handle multiple rcpt_to 2020-03-28 21:24:43 +01:00
Son NK
e9cd043760 do not put alias again when reply all 2020-03-28 21:20:59 +01:00
Son NK
aa3a13c3ca Replace To or CC header when forward/reply 2020-03-28 19:16:55 +01:00
Son NK
5b9f3c2763 add contact.is_cc column 2020-03-28 19:05:27 +01:00
Son NK
fee69d9546 refactor: create generate_reply_email() 2020-03-28 11:12:20 +01:00
Son NK
f10d18c020 Add SL prefix to 550 to facilitate search 2020-03-28 11:05:14 +01:00
Son NK
9afcae534b return 550 instead of 510 when alias not exist 2020-03-28 11:04:58 +01:00
Son NK
0be0a180f7 Replace logo 2020-03-27 10:11:21 +01:00
Son Nguyen Kim
b3d99cc010
Merge pull request #119 from simple-login/api-alias
Api alias
2020-03-26 22:14:17 +01:00
Son NK
97dff83453 return full alias info in POST /api/alias/random/new 2020-03-26 19:50:22 +01:00
Son NK
d4a32451c1 return full alias info in POST /api/alias/custom/new 2020-03-26 19:48:36 +01:00
Son NK
159aa76aae GET /api/aliases/:alias_id 2020-03-26 19:44:00 +01:00
Son NK
78b24623af reformat: create serialize_alias_info() 2020-03-26 19:35:44 +01:00
Son NK
0c43c2dd45 reformat: rename website -> contact whenever possible 2020-03-26 11:19:20 +01:00
Son NK
c521052042 use {website_email} via SimpleLogin FROM header 2020-03-26 11:15:18 +01:00
Son NK
94afb28ce2 Add run init data to upgrade 2020-03-25 12:13:49 +01:00
Son NK
a6b1372e84 v2.1.0 2020-03-25 12:13:43 +01:00
Son NK
a59058b163 add how to upgrade 2.0.0 -> 2.1.0 2020-03-25 12:13:28 +01:00
Son NK
c603b376a5 add /sl mounting 2020-03-25 12:13:22 +01:00
Son NK
bfcb02c3b0 Add SSL doc 2020-03-25 12:13:13 +01:00
Son NK
60d519b1d4 Set mydestination to empty to fix case where myhostname is the same as mydomain 2020-03-25 12:12:50 +01:00
Son NK
2261cd2138 add send_pgp_newsletter() to shell 2020-03-24 21:31:54 +01:00
Son NK
0acde29443 mailbox is move to 2nd onboarding, pgp 3rd onboarding 2020-03-24 21:23:26 +01:00
Son NK
f4d28a998a fix email wording 2020-03-24 21:21:55 +01:00
Son NK
9e04081186 send mailbox onboarding email 2020-03-24 21:19:45 +01:00
Son NK
c02b8298fc send pgp onboarding email to user on 2nd day 2020-03-24 21:01:38 +01:00
Son NK
929e965f23 rename onboarding-1 to onboarding/send-from-alias 2020-03-24 20:55:50 +01:00
Son NK
3f1716d9ec pgp newsletter 2020-03-24 20:12:56 +01:00
Son NK
f99f656872 increase max-width to 750px (old value 570px), reduce padding to 30px (old value: 45px) 2020-03-24 20:11:55 +01:00
Son NK
eae2efb5d9 trigger build 2020-03-23 00:32:14 +01:00
Son NK
bf98fa00b7 Fix alias display 2020-03-22 23:30:55 +01:00
Son Nguyen Kim
f3a70ca909
Merge pull request #118 from simple-login/tmp
Handle empty bounce email
2020-03-22 23:28:46 +01:00
Son NK
bc3a3dae02 reformat 2020-03-22 16:56:08 +01:00
Son NK
86ef7f54d9 RefusedEmail.path can be null 2020-03-22 16:51:21 +01:00
Son NK
88039844ef remove "subject" from DKIM 2020-03-22 15:08:26 +01:00
Son NK
22ac3fa153 strip from/to header 2020-03-22 14:21:19 +01:00
Son NK
b8093aefa3 Handle invalid email when user signs up 2020-03-21 11:11:52 +01:00
Son NK
8abdf655fc make aliasUsedOn.user_id non nullable 2020-03-20 12:29:37 +01:00
Son NK
7d4a9efb5d set user_id when creating AliasUsedOn 2020-03-20 12:29:11 +01:00
Son NK
92de2102ad Add AliasUsedOn.user_id col 2020-03-20 12:13:00 +01:00
Son NK
5d0b4d4aca Fix: filter email log by current user 2020-03-20 11:39:45 +01:00
Son NK
783b1937d5 make user_id non nullable on contact, email_log and file 2020-03-20 10:17:52 +01:00
Son NK
5400631d77 fix migration 2020-03-20 10:17:52 +01:00
Son NK
7f5f3e68ba make sure to set user_id when creating EmailLog 2020-03-20 09:55:52 +01:00
Son NK
6e54b4fed8 make sure to set user_id when creating contact 2020-03-20 09:54:38 +01:00
Son NK
abd2278c24 make sure to set File.user_id 2020-03-20 09:52:00 +01:00
Son NK
cd19997424 Add File.user_id, Contact.user_id, EmailLog.user_id columns 2020-03-20 09:51:15 +01:00
Son NK
7f75e05148 mention mailbox in trial-end email 2020-03-19 19:21:47 +01:00
Son NK
5f6cef4c96 Mention PGP in trial end email 2020-03-19 19:20:44 +01:00
Son NK
8ce9d56e84 only premium user can encrypt emails with PGP 2020-03-19 19:19:04 +01:00
Son NK
a529943dc4 Only premium user can add PGP key 2020-03-19 19:15:42 +01:00
Son NK
c6138828c2 fix to_header 2020-03-19 11:33:21 +01:00
Son NK
ca4f02426c fix alias != address 2020-03-19 11:15:02 +01:00
Son NK
edcdbabc93 fix migration 2020-03-19 10:54:37 +01:00
Son Nguyen Kim
44e2e175ef
Merge pull request #115 from simple-login/rename
Rename
2020-03-19 10:39:05 +01:00
Son NK
3f81b92d61 Add PGP to welcome email 2020-03-18 22:29:30 +01:00
Son NK
b0f2d7b85a always return 200 in /forgot_password 2020-03-18 21:55:50 +01:00
Son NK
a1fad2216f Improve error 2020-03-18 21:37:45 +01:00
Son NK
2079b16431 add "in_trial" to /user_info 2020-03-18 19:08:16 +01:00
Son NK
f2d5230449 Add POST /api/auth/forgot_password 2020-03-18 18:43:04 +01:00
Son NK
ba6b8d2711 return email in /user_info 2020-03-18 18:34:37 +01:00
Son NK
aaf254a26d order alias by creation date 2020-03-17 21:47:11 +01:00
Son Nguyen Kim
6cec75a066
Merge pull request #116 from simple-login/misc
Api Improvements
2020-03-17 20:19:46 +01:00
Son NK
a521002b2c Fix duplicate results 2020-03-17 20:16:20 +01:00
Son NK
a465b1d3ca rename get_alias_info -> get_alias_infos 2020-03-17 19:59:48 +01:00
Son NK
fbef076a14 Take into account query in GET /api/aliases 2020-03-17 19:32:45 +01:00
Son NK
0d725588ae Add DELETE /api/contacts/:contact_id 2020-03-17 19:18:26 +01:00
Son NK
81b5e919a3 Return contact id 2020-03-17 12:38:50 +01:00
Son NK
4a2523d20e refactor handle_bounce 2020-03-17 12:12:11 +01:00
Son NK
a597fb3832 fix should_append_alias 2020-03-17 12:10:13 +01:00
Son NK
ac27ea5847 Improve UI 2020-03-17 12:06:26 +01:00
Son NK
2d90d35647 rename gen_email_id -> alias_id 2020-03-17 12:01:18 +01:00
Son NK
ab16f1afeb rename table gen_email -> alias 2020-03-17 11:52:47 +01:00
Son NK
4f281bdbbb rename GenEmail -> Alias, gen_email to alias whenever possible 2020-03-17 11:51:40 +01:00
Son NK
bea870ef8b rename ForwardEmailLog to EmailLog 2020-03-17 11:10:50 +01:00
Son NK
f2d630e597 rename forward_id to contact_id 2020-03-17 11:05:53 +01:00
Son NK
578d09c26c rename forward_email table to contact 2020-03-17 10:57:55 +01:00
Son NK
17974de746 rename ForwardEmail to Contact 2020-03-17 10:56:59 +01:00
Son Nguyen Kim
269bca8ff3
Merge pull request #114 from simple-login/pgp-for-everyone
Open PGP to everyone
2020-03-17 09:53:42 +01:00
Son NK
cc6e8a00a5 Wording: rename refused email to Quarantine 2020-03-17 09:43:12 +01:00
Son NK
ea43b8f685 Open PGP to everyone 2020-03-17 09:22:29 +01:00
Son NK
8faf34ce40 Use warning level for /v2/alias/options 2020-03-16 21:10:53 +01:00
Son Nguyen Kim
1e10b4d675
Merge pull request #113 from simple-login/fix-custom-domain-not-set
Fix custom domain not set
2020-03-15 23:38:25 +01:00
Son NK
ccc005b8b2 Do not set domain for ALIAS_DOMAINS 2020-03-15 23:33:50 +01:00
Son NK
93292c88c9 Fix not setting custom domain when creating alias via API 2020-03-15 23:18:43 +01:00
Son Nguyen Kim
f049960c81
Merge pull request #112 from simple-login/fix-email
Fix email To header
2020-03-15 23:18:00 +01:00
Son NK
ee1642bf99 use a more consistent format 2020-03-15 23:10:20 +01:00
Son NK
17f3c112b9 do not use formataddr to generate website email 2020-03-15 23:02:06 +01:00
Son NK
027cbb10d9 use parseaddr instead of get_email_part 2020-03-15 22:32:48 +01:00
Son NK
6b6fca2281 use email.utils.parseaddr and formataddr instead of get_email_name 2020-03-15 22:29:53 +01:00
Son NK
ed42a16b8e Setup precommit 2020-03-15 19:16:00 +01:00
Son Nguyen Kim
93765335ed
Merge pull request #110 from simple-login/refused-email
Handle refused email
2020-03-15 19:08:35 +01:00
Son NK
b19be41a5e Support download email file in browser 2020-03-15 18:39:59 +01:00
Son NK
eb3063a57f Improve wording 2020-03-15 18:06:57 +01:00
Son NK
b3977e5efd reformat 2020-03-15 12:26:35 +01:00
Son NK
4b21f49f49 add mailbox email into notif email 2020-03-15 12:25:01 +01:00
Son NK
9cdf766825 Send refused email notif to user email instead of mailbox 2020-03-15 12:15:11 +01:00
Son NK
45d560fd70 fix 2020-03-15 12:14:43 +01:00
Son NK
e21e27eefa Hide download for deleted refused emails 2020-03-15 11:14:58 +01:00
Son NK
71a9fc38a9 Add cronjob to delete refused emails 2020-03-15 11:11:16 +01:00
Son NK
a923d9ad6a Add refused_email.deleted column 2020-03-15 11:10:37 +01:00
Son NK
0525e5822a Not include original email in automatic disable alias email 2020-03-15 10:50:46 +01:00
Son NK
5db92b049d Inform refused email to mailbox 2020-03-14 23:00:33 +01:00
Son NK
69198ff08a delete all unnecessary headers in PGP 2020-03-14 22:24:02 +01:00
Son NK
7bf1baaafa Add sql migration 2020-03-14 16:36:18 +01:00
Son NK
0bb9830680 Store the bounced email in email handling. 2020-03-14 16:34:23 +01:00
Son NK
c3b85115ca Add refused-email view 2020-03-14 16:19:56 +01:00
Son NK
0de13ca4d9 add RefusedEmail model 2020-03-14 16:07:34 +01:00
Son NK
28c6c68a80 Use a better mailbox validation page 2020-03-14 14:45:37 +01:00
Son Nguyen Kim
67abc08f45
Merge pull request #109 from simple-login/alias-api
Alias api
2020-03-14 13:58:20 +01:00
Son NK
f87746e071 POST /api/aliases/:alias_id/contacts 2020-03-14 12:55:38 +01:00
Son NK
d7cb5ed26e GET /api/aliases/:alias_id/contacts 2020-03-14 12:22:43 +01:00
Son NK
bfd729b889 PUT /api/aliases/:alias_id 2020-03-14 11:38:39 +01:00
Son NK
7429330fe7 Re-organize upgrade.md 2020-03-13 18:43:46 +01:00
Son Nguyen Kim
de67a1a7aa
Merge pull request #108 from simple-login/local-upload
Add LOCAL_FILE_UPLOAD param
2020-03-13 18:36:42 +01:00
Son NK
a9fdfc799f Add LOCAL_FILE_UPLOAD param 2020-03-13 14:37:48 +01:00
Son NK
cf257a92ec use utf-8 for PGP 2020-03-13 12:54:52 +01:00
Son NK
c369742706 v2.0.0 2020-03-13 12:34:07 +01:00
Son NK
035d5e5f7d Add migration instruction 2020-03-13 12:32:48 +01:00
Son Nguyen Kim
f86506e697
Merge pull request #107 from simple-login/fix-sender
Fix sender
2020-03-13 12:01:11 +01:00
Son NK
d0616d05a5 Add Sender fix to Changelog 2020-03-13 10:35:56 +01:00
Son NK
e713efee98 reformat 2020-03-13 10:34:41 +01:00
Son NK
be9d2cdbe3 delete sender header 2020-03-13 10:34:02 +01:00
879 changed files with 82327 additions and 379493 deletions

View File

@ -6,5 +6,12 @@ db.sqlite
.vscode .vscode
.DS_Store .DS_Store
config config
LICENSE adhoc
README.md static/node_modules
db.sqlite-journal
static/upload
venv/
.venv
.coverage
htmlcov
.git/

26
.flake8 Normal file
View File

@ -0,0 +1,26 @@
[flake8]
max-line-length = 88
select = C,E,F,W,B,B902,B903,B904,B950
extend-ignore =
# For black compatibility
E203,
E501,
# Ignore "f-string is missing placeholders"
F541,
# allow bare except
E722, B001
exclude =
.git,
__pycache__,
.pytest_cache,
.venv,
static,
templates,
# migrations are generated by alembic
migrations,
docs,
shell.py
per-file-ignores =
# ignore unused imports in __init__
__init__.py:F401

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
# https://github.com/github/linguist#overrides
static/* linguist-vendored
docs/* linguist-documentation

2
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,2 @@
## code changes will send PR to following users
* @acasajus @cquintana92 @nguyenkims

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
patreon: simplelogin open_collective: simplelogin

39
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,39 @@
---
name: Bug report
about: Create a report to help us improve SimpleLogin.
title: ''
labels: ''
assignees: ''
---
Please note that this is only for bug report.
For help on your account, please reach out to us at hi[at]simplelogin.io. Please make sure to check out [our FAQ](https://simplelogin.io/faq/) that contains frequently asked questions.
For feature request, you can use our [forum](https://github.com/simple-login/app/discussions/categories/feature-request).
For self-hosted question/issue, please ask in [self-hosted forum](https://github.com/simple-login/app/discussions/categories/self-hosting-question)
## Prerequisites
- [ ] I have searched open and closed issues to make sure that the bug has not yet been reported.
## Bug report
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment (If applicable):**
- OS: Linux, Mac, Windows
- Browser: Firefox, Chrome, Brave, Safari
- Version [e.g. 78]
**Additional context**
Add any other context about the problem here.

23
.github/changelog_configuration.json vendored Normal file
View File

@ -0,0 +1,23 @@
{
"template": "${{CHANGELOG}}\n\n<details>\n<summary>Uncategorized</summary>\n\n${{UNCATEGORIZED}}\n</details>",
"pr_template": "- ${{TITLE}} #${{NUMBER}}",
"empty_template": "- no changes",
"categories": [
{
"title": "## 🚀 Features",
"labels": ["feature"]
},
{
"title": "## 🐛 Fixes",
"labels": ["fix", "bug"]
},
{
"title": "## 🔧 Enhancements",
"labels": ["enhancement"]
}
],
"ignore_labels": ["ignore"],
"tag_resolver": {
"method": "semver"
}
}

View File

@ -1,48 +1,244 @@
name: Run tests & Public to Docker Registry name: Test and lint
on: [push] on: [push, pull_request]
jobs: jobs:
build: lint:
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@v3
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: 'poetry'
- name: Install OS dependencies
if: ${{ matrix.python-version }} == '3.10'
run: |
sudo apt update
sudo apt install -y libre2-dev libpq-dev
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction
- name: Check formatting & linting
run: |
poetry run pre-commit run --all-files
test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
max-parallel: 4 max-parallel: 4
matrix: matrix:
python-version: [3.7] python-version: ["3.10"]
# service containers to run with `postgres-job`
services:
# label used to access the service container
postgres:
# Docker Hub image
image: postgres:13
# service environment variables
# `POSTGRES_HOST` is `postgres`
env:
# optional (defaults to `postgres`)
POSTGRES_DB: test
# required
POSTGRES_PASSWORD: test
# optional (defaults to `5432`)
POSTGRES_PORT: 5432
# optional (defaults to `postgres`)
POSTGRES_USER: test
ports:
- 15432:5432
# set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps: steps:
- uses: actions/checkout@v1 - name: Check out repo
- name: Set up Python ${{ matrix.python-version }} uses: actions/checkout@v3
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Test formatting - name: Install poetry
run: | run: pipx install poetry
pip install black
black --check .
- name: Install dependencies - uses: actions/setup-python@v4
run: | with:
python -m pip install --upgrade pip python-version: ${{ matrix.python-version }}
pip install -r requirements.txt cache: 'poetry'
- name: Test with pytest - name: Install OS dependencies
run: | if: ${{ matrix.python-version }} == '3.10'
pip install pytest run: |
pytest sudo apt update
sudo apt install -y libre2-dev libpq-dev
- name: Publish to Docker Registry - name: Install dependencies
uses: elgohr/Publish-Docker-Github-Action@master if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
with: run: poetry install --no-interaction
name: simplelogin/app-ci
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Send Telegram message
uses: appleboy/telegram-action@master - name: Start Redis v6
with: uses: superchargejs/redis-github-action@1.1.0
to: ${{ secrets.TELEGRAM_TO }} with:
token: ${{ secrets.TELEGRAM_TOKEN }} redis-version: 6
args: Docker image pushed on ${{ github.ref }}
- name: Run db migration
run: |
CONFIG=tests/test.env poetry run alembic upgrade head
- name: Prepare version file
run: |
scripts/generate-build-info.sh ${{ github.sha }}
cat app/build_info.py
- name: Test with pytest
run: |
poetry run pytest
env:
GITHUB_ACTIONS_TEST: true
- name: Archive code coverage results
uses: actions/upload-artifact@v4
with:
name: code-coverage-report
path: htmlcov
build:
runs-on: ubuntu-latest
needs: ['test', 'lint']
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v'))
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: simplelogin/app-ci
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# We need to checkout the repository in order for the "Create Sentry release" to work
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Create Sentry release
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
with:
ignore_missing: true
ignore_empty: true
- name: Prepare version file
run: |
scripts/generate-build-info.sh ${{ github.sha }}
cat app/build_info.py
- name: Build image and publish to Docker Registry
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
#- name: Send Telegram message
# uses: appleboy/telegram-action@master
# with:
# to: ${{ secrets.TELEGRAM_TO }}
# token: ${{ secrets.TELEGRAM_TOKEN }}
# args: Docker image pushed on ${{ github.ref }}
# If we have generated a tag, generate the changelog, send a notification to slack and create the GitHub release
- name: Build Changelog
id: build_changelog
if: startsWith(github.ref, 'refs/tags/v')
uses: mikepenz/release-changelog-builder-action@v3
with:
configuration: ".github/changelog_configuration.json"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare Slack notification contents
if: startsWith(github.ref, 'refs/tags/v')
run: |
changelog=$(cat << EOH
${{ steps.build_changelog.outputs.changelog }}
EOH
)
messageWithoutNewlines=$(echo "${changelog}" | awk '{printf "%s\\n", $0}')
messageWithoutDoubleQuotes=$(echo "${messageWithoutNewlines}" | sed "s/\"/'/g")
echo "${messageWithoutDoubleQuotes}"
echo "SLACK_CHANGELOG=${messageWithoutDoubleQuotes}" >> $GITHUB_ENV
- name: Post notification to Slack
uses: slackapi/slack-github-action@v1.19.0
if: startsWith(github.ref, 'refs/tags/v')
with:
channel-id: ${{ secrets.SLACK_CHANNEL_ID }}
payload: |
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "New tag created",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Tag: ${{ github.ref_name }}* (${{ github.sha }})"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Changelog:*\n${{ env.SLACK_CHANGELOG }}"
}
}
]
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/create-release@v1
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: ${{ steps.build_changelog.outputs.changelog }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

9
.gitignore vendored
View File

@ -7,4 +7,11 @@ db.sqlite
.DS_Store .DS_Store
config config
static/node_modules static/node_modules
db.sqlite-journal db.sqlite-journal
static/upload
venv/
.venv
.coverage
htmlcov
adhoc
.env.*

3
.jshintrc Normal file
View File

@ -0,0 +1,3 @@
{
"esversion": 8
}

24
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,24 @@
exclude: "(migrations|static/node_modules|static/assets|static/vendor)"
default_language_version:
python: python3
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
hooks:
- id: check-yaml
- id: trailing-whitespace
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.34.1
hooks:
- id: djlint-jinja
files: '.*\.html'
entry: djlint --reformat
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.5
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format

227
.pylintrc Normal file
View File

@ -0,0 +1,227 @@
[MASTER]
extension-pkg-allow-list=re2
fail-under=7.0
ignore=CVS
ignore-paths=migrations
ignore-patterns=^\.#
jobs=0
[MESSAGES CONTROL]
disable=missing-function-docstring,
missing-module-docstring,
duplicate-code,
#import-error,
missing-class-docstring,
useless-object-inheritance,
use-dict-literal,
logging-format-interpolation,
consider-using-f-string,
unnecessary-comprehension,
inconsistent-return-statements,
wrong-import-order,
line-too-long,
invalid-name,
global-statement,
no-else-return,
unspecified-encoding,
logging-fstring-interpolation,
too-few-public-methods,
bare-except,
fixme,
unnecessary-pass,
f-string-without-interpolation,
super-init-not-called,
unused-argument,
ungrouped-imports,
too-many-locals,
consider-using-with,
too-many-statements,
consider-using-set-comprehension,
unidiomatic-typecheck,
useless-else-on-loop,
too-many-return-statements,
broad-except,
protected-access,
consider-using-enumerate,
too-many-nested-blocks,
too-many-branches,
simplifiable-if-expression,
possibly-unused-variable,
pointless-string-statement,
wrong-import-position,
redefined-outer-name,
raise-missing-from,
logging-too-few-args,
redefined-builtin,
too-many-arguments,
import-outside-toplevel,
redefined-argument-from-local,
logging-too-many-args,
too-many-instance-attributes,
unreachable,
no-name-in-module,
no-member,
consider-using-ternary,
too-many-lines,
arguments-differ,
too-many-public-methods,
unused-variable,
consider-using-dict-items,
consider-using-in,
reimported,
too-many-boolean-expressions,
cyclic-import,
not-callable, # (paddle_utils.py) verifier.verify cannot be called (although it can)
abstract-method, # (models.py)
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[FORMAT]
max-line-length=88
single-line-if-stmt=yes

1
.version Normal file
View File

@ -0,0 +1 @@
dev

View File

@ -6,11 +6,99 @@ The version corresponds to SimpleLogin Docker `image tag`.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [3.4.0] - 2021-04-06
Support ARM arch
Remove unused config like DEBUG, CLOUDWATCH, DKIM_PUBLIC_KEY_PATH, DKIM_DNS_VALUE
Handle auto responder email
Inform user when their alias has been transferred to another user
Use alias transfer_token
Improve logging
Add /api/export/data, /api/export/aliases endpoints
Take into account mailbox when importing/exporting aliases
Multiple bug fixes
Code refactoring
Add ENABLE_SPAM_ASSASSIN config
## [3.3.0] - 2021-03-05
Notify user when reply cannot be sent
User can choose default domain for random alias
enable LOCAL_FILE_UPLOAD by default
fix user has to login again after quitting the browser
login user in api auth endpoints
Create POST /api/api_key
Add GET /api/logout
Add setup-done page
Add PublicDomain
User can choose a random alias domain in a list of public domains
User can choose mailboxes for a domain
Return support_pgp in GET /api/v2/aliases
Self hosting improvements
Improve Search
Use poetry instead of pip
Add PATCH /api/user_info
Add GET /api/setting
Add GET /api/setting/domains
Add PATCH /api/setting
Add "Generic Subject" option
Add /v2/setting/domains
Add /api/v5/alias/options
Add GET /api/custom_domains
Add GET /api/custom_domains/:custom_domain_id/trash
Able to disable a directory
Use VERP: send email from bounce address
Use VERP for transactional email: remove SENDER, SENDER_DIR
Use "John Wick - john at wick.com" as default sender format
Able to transfer an alias
## [3.2.2] - 2020-06-15
Fix POST /v2/alias/custom/new when DISABLE_ALIAS_SUFFIX is set
## [3.2.1] - 2020-06-15
Fix regressions introduced in 3.2.0 regarding DISABLE_ALIAS_SUFFIX option
## [3.2.0] - 2020-06-10
Make FIDO available
Fix "remove the reverse-alias" when replying
Update GET /mailboxes
Create POST /api/v3/alias/custom/new
Add PGP for contact
## [3.1.1] - 2020-05-27
Fix alias creation
## [3.1.0] - 2020-05-09
Remove social login signup
More simple UI with advanced options hidden by default
Use pagination for alias page
Use Ajax for alias note and mailbox update
Alias can have a name
Global stats
DMARC support for custom domain
Enforce SPF
FIDO support (beta)
Able to disable onboarding emails
## [3.0.1] - 2020-04-13
Fix compatibility with 2x version
Fix "Content-Transfer-Encoding" issue https://github.com/simple-login/app/issues/125
## [3.0.0] - 2020-04-13
New endpoints to create/update aliases:
PUT /api/aliases/:alias_id
GET /api/aliases/:alias_id/contacts
POST /api/aliases/:alias_id/contacts
GET /api/v2/aliases
(Optional) Spam detection by Spamassassin
Handling for bounced emails
Support Multiple recipients (in To and Cc headers)
## [2.1.0] - 2020-03-23
Support PGP Support PGP
## [1.1.0] - 2020-03-13 ## [2.0.0] - 2020-03-13
Support multiple Mailboxes Support multiple Mailboxes
Take into account Sender header
## [1.0.5] - 2020-02-24 ## [1.0.5] - 2020-02-24
Improve email forwarding. Improve email forwarding.
@ -29,7 +117,7 @@ Add SUPPORT_NAME param to set a support email name.
## [1.0.1] - 2020-01-28 ## [1.0.1] - 2020-01-28
Simplify config file. Simplify config file.
## [1.0.0] - 2020-01-22 ## [1.0.0] - 2020-01-22

253
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,253 @@
Thanks for taking the time to contribute! 🎉👍
Before working on a new feature, please get in touch with us at dev[at]simplelogin.io to avoid duplication.
We can also discuss the best way to implement it.
The project uses Flask, Python3.7+ and requires Postgres 12+ as dependency.
## General Architecture
<p align="center">
<img src="./docs/archi.png" height="450px">
</p>
SimpleLogin backend consists of 2 main components:
- the `webapp` used by several clients: the web app, the browser extensions (Chrome & Firefox for now), OAuth clients (apps that integrate "Sign in with SimpleLogin" button) and mobile apps.
- the `email handler`: implements the email forwarding (i.e. alias receiving email) and email sending (i.e. alias sending email).
## Install dependencies
The project requires:
- Python 3.10 and poetry to manage dependencies
- Node v10 for front-end.
- Postgres 13+
First, install all dependencies by running the following command.
Feel free to use `virtualenv` or similar tools to isolate development environment.
```bash
poetry sync
```
On Mac, sometimes you might need to install some other packages via `brew`:
```bash
brew install pkg-config libffi openssl postgresql@13
```
You also need to install `gpg` tool, on Mac it can be done with:
```bash
brew install gnupg
```
If you see the `pyre2` package in the error message, you might need to install its dependencies with `brew`.
More info on https://github.com/andreasvc/pyre2
```bash
brew install -s re2 pybind11
```
## Linting and static analysis
We use pre-commit to run all our linting and static analysis checks. Please run
```bash
poetry run pre-commit install
```
To install it in your development environment.
## Run tests
For most tests, you will need to have ``redis`` installed and started on your machine (listening on port 6379).
```bash
sh scripts/run-test.sh
```
You can also run tests using a local Postgres DB to speed things up. This can be done by
- creating an empty test DB and running the database migration by `dropdb test && createdb test && DB_URI=postgresql://localhost:5432/test alembic upgrade head`
- replacing the `DB_URI` in `test.env` file by `DB_URI=postgresql://localhost:5432/test`
## Run the code locally
Install npm packages
```bash
cd static && npm install
```
To run the code locally, please create a local setting file based on `example.env`:
```
cp example.env .env
```
You need to edit your .env to reflect the postgres exposed port, edit the `DB_URI` to:
```
DB_URI=postgresql://myuser:mypassword@localhost:35432/simplelogin
```
Run the postgres database:
```bash
docker run -e POSTGRES_PASSWORD=mypassword -e POSTGRES_USER=myuser -e POSTGRES_DB=simplelogin -p 15432:5432 postgres:13
```
To run the server:
```
alembic upgrade head && flask dummy-data && python3 server.py
```
then open http://localhost:7777, you should be able to login with `john@wick.com / password` account.
You might need to change the `.env` file for developing certain features. This file is ignored by git.
## Database migration
The database migration is handled by `alembic`
Whenever the model changes, a new migration has to be created.
If you have Docker installed, you can create the migration by the following script:
```bash
sh scripts/new-migration.sh
```
Make sure to review the migration script before committing it.
Sometimes (very rarely though), the automatically generated script can be incorrect.
We cannot use the local database to generate migration script as the local database doesn't use migration.
It is created via `db.create_all()` (cf `fake_data()` method). This is convenient for development and
unit tests as we don't have to wait for the migration.
## Reset database
There are two scripts to reset your local db to an empty state:
- `scripts/reset_local_db.sh` will reset your development db to the latest migration version and add the development data needed to run the
server.py locally.
- `scripts/reset_test_db.sh` will reset your test db to the latest migration without adding the dev server data to prevent interferring with
the tests.
## Code structure
The repo consists of the three following entry points:
- wsgi.py and server.py: the webapp.
- email_handler.py: the email handler.
- cron.py: the cronjob.
Here are the small sum-ups of the directory structures and their roles:
- app/: main Flask app. It is structured into different packages representing different features like oauth, api, dashboard, etc.
- local_data/: contains files to facilitate the local development. They are replaced during the deployment.
- migrations/: generated by flask-migrate. Edit these files will be only edited when you spot (very rare) errors on the database migration files.
- static/: files available at `/static` url.
- templates/: contains both html and email templates.
- tests/: tests. We don't really distinguish unit, functional or integration test. A test is simply here to make sure a feature works correctly.
## Pull request
The code is formatted using [ruff](https://github.com/astral-sh/ruff), to format the code, simply run
```
poetry run ruff format .
```
The code is also checked with `flake8`, make sure to run `flake8` before creating the pull request by
```bash
poetry run flake8
```
For HTML templates, we use `djlint`. Before creating a pull request, please run
```bash
poetry run djlint --check templates
```
If some files aren't properly formatted, you can format all files with
```bash
poetry run djlint --reformat .
```
## Test sending email
[swaks](http://www.jetmore.org/john/code/swaks/) is used for sending test emails to the `email_handler`.
[mailcatcher](https://github.com/sj26/mailcatcher) or [MailHog](https://github.com/mailhog/MailHog) can be used as a MTA to receive emails.
Here's how set up the email handler:
1) run mailcatcher or MailHog
```bash
mailcatcher
```
2) Make sure to set the following variables in the `.env` file
```
# comment out this variable
# NOT_SEND_EMAIL=true
# So the emails will be sent to mailcatcher/MailHog
POSTFIX_SERVER=localhost
POSTFIX_PORT=1025
```
3) Run email_handler
```bash
python email_handler.py
```
4) Send a test email
```bash
swaks --to e1@sl.local --from hey@google.com --server 127.0.0.1:20381
```
Now open http://localhost:1080/ (or http://localhost:1080/ for MailHog), you should see the forwarded email.
## Job runner
Some features require a job handler (such as GDPR data export). To test such feature you need to run the job_runner
```bash
python job_runner.py
```
# Setup for Mac
There are several ways to setup Python and manage the project dependencies on Mac. For info we have successfully used this setup on a Mac silicon:
```bash
# we haven't managed to make python 3.12 work
brew install python3.10
# make sure to update the PATH so python, pip point to Python3
# for us it can be done by adding "export PATH=/opt/homebrew/opt/python@3.10/libexec/bin:$PATH" to .zprofile
# Although pipx is the recommended way to install poetry,
# install pipx via brew will automatically install python 3.12
# and poetry will then use python 3.12
# so we recommend using poetry this way instead
curl -sSL https://install.python-poetry.org | python3 -
poetry install
# activate the virtualenv and you should be good to go!
source .venv/bin/activate
```

View File

@ -2,15 +2,38 @@
FROM node:10.17.0-alpine AS npm FROM node:10.17.0-alpine AS npm
WORKDIR /code WORKDIR /code
COPY ./static/package*.json /code/static/ COPY ./static/package*.json /code/static/
RUN cd /code/static && npm install RUN cd /code/static && npm ci
# Main image
FROM python:3.10
# Keeps Python from generating .pyc files in the container
ENV PYTHONDONTWRITEBYTECODE 1
# Turns off buffering for easier container logging
ENV PYTHONUNBUFFERED 1
# Add poetry to PATH
ENV PATH="${PATH}:/root/.local/bin"
FROM python:3.7
WORKDIR /code WORKDIR /code
# install dependencies # Copy poetry files
COPY ./requirements.txt ./ COPY poetry.lock pyproject.toml ./
RUN pip3 install --no-cache-dir -r requirements.txt
# Install and setup poetry
RUN pip install -U pip \
&& apt-get update \
&& apt install -y curl netcat-traditional gcc python3-dev gnupg git libre2-dev cmake ninja-build\
&& curl -sSL https://install.python-poetry.org | python3 - \
# Remove curl and netcat from the image
&& apt-get purge -y curl netcat-traditional \
# Run poetry
&& poetry config virtualenvs.create false \
&& poetry install --no-interaction --no-ansi --no-root \
# Clear apt cache \
&& apt-get purge -y libre2-dev cmake ninja-build\
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# copy npm packages # copy npm packages
COPY --from=npm /code /code COPY --from=npm /code /code

674
LICENSE
View File

@ -1,21 +1,661 @@
MIT License GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (c) 2020 SimpleLogin Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy Preamble
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The GNU Affero General Public License is a free, copyleft license for
copies or substantial portions of the Software. software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR The licenses for most software and other practical works are designed
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, to take away your freedom to share and change the works. By contrast,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE our General Public Licenses are intended to guarantee your freedom to
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER share and change all versions of a program--to make sure it remains free
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, software for all its users.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

728
README.md
View File

@ -1,96 +1,42 @@
<p align="center">
<a href="https://simplelogin.io">
<img src="./docs/diagram.png" height="300px">
</a>
</p>
[SimpleLogin](https://simplelogin.io) | Privacy-First Email Forwarding and Identity Provider Service
[SimpleLogin](https://simplelogin.io) | Protect your online identity with email alias
--- ---
<p> <p>
<a href="https://chrome.google.com/webstore/detail/simplelogin-protect-your/dphilobhebphkdjbpfohgikllaljmgbn"> <a href="https://chrome.google.com/webstore/detail/dphilobhebphkdjbpfohgikllaljmgbn">
<img src="https://img.shields.io/chrome-web-store/rating/dphilobhebphkdjbpfohgikllaljmgbn?label=Chrome%20Extension"> <img src="https://img.shields.io/chrome-web-store/rating/dphilobhebphkdjbpfohgikllaljmgbn?label=Chrome%20Extension">
</a> </a>
<a href="https://addons.mozilla.org/en-GB/firefox/addon/simplelogin/"> <a href="https://addons.mozilla.org/firefox/addon/simplelogin/">
<img src="https://img.shields.io/amo/rating/simplelogin?label=Firefox%20Add-On&logo=SimpleLogin"> <img src="https://img.shields.io/amo/rating/simplelogin?label=Firefox%20Add-On&logo=SimpleLogin">
</a> </a>
<a href="https://stats.uptimerobot.com/APkzziNWoM">
<img src="https://img.shields.io/uptimerobot/ratio/7/m782965045-15d8e413b20b5376f58db050">
</a>
<a href="./LICENSE"> <a href="./LICENSE">
<img src="https://img.shields.io/github/license/simple-login/app"> <img src="https://img.shields.io/github/license/simple-login/app">
</a> </a>
<a href="https://twitter.com/simple_login"> <a href="https://twitter.com/simplelogin">
<img src="https://img.shields.io/twitter/follow/simple_login?style=social"> <img src="https://img.shields.io/twitter/follow/simplelogin?style=social">
</a> </a>
</p> </p>
> Yet another email forwarding service?
In some way yes... However, SimpleLogin is a bit different because:
- Fully open source: both the server and client code (browser extension, JS library) are open source so anyone can freely inspect and (hopefully) improve the code.
- The only email forwarding solution that is **self-hostable**: with our detailed self-hosting instructions and most of components running as Docker container, anyone who knows `ssh` is able to deploy SimpleLogin on their server.
- Not just email alias: SimpleLogin is a privacy-first and developer-friendly identity provider that:
- offers privacy for users
- is simple to use for developers. SimpleLogin is a privacy-focused alternative to the "Login with Facebook/Google/Twitter" buttons.
- Plenty of features: browser extension, custom domain, catch-all alias, OAuth libraries, etc.
- Open roadmap at https://trello.com/b/4d6A69I4/open-roadmap: you know the exciting features we are working on.
At the heart of SimpleLogin is `email alias`: an alias is a normal email address but all emails sent to an alias are **forwarded** to your email inbox. SimpleLogin alias can also **send** emails: for your contact, the alias is therefore your email address. Use alias whenever you need to give out your email address to protect your online identity. More info on our website at https://simplelogin.io
<p align="center"> <p align="center">
<img src="./docs/custom-alias.png" height="150px"> <a href="https://simplelogin.io">
<img src="./docs/hero.png" height="600px">
</a>
</p> </p>
# Quick start ---
If you have Docker installed, run the following command to start SimpleLogin local server: Your email address is your **online identity**. When you use the same email address everywhere, you can be easily tracked.
More information on https://simplelogin.io
This README contains instructions on how to self host SimpleLogin.
```bash Once you have your own SimpleLogin instance running, you can change the `API URL` in SimpleLogin's Chrome/Firefox extension, Android/iOS app to your server.
docker run --name sl -it --rm \
-e RESET_DB=true \
-e CONFIG=/code/example.env \
-p 7777:7777 \
simplelogin/app:1.1.0 python server.py
```
Then open http://localhost:7777, you should be able to login with `john@wick.com/password` account! SimpleLogin roadmap is at https://github.com/simple-login/app/projects/1 and our forum at https://github.com/simple-login/app/discussions, feel free to submit new ideas or vote on features.
To use SimpleLogin aliases, you need to deploy it on your server with some DNS setup though,
the following section will show a step-by-step guide on how to get your own email forwarder service!
# Table of Contents
[1. General Architecture](#general-architecture)
[2. Self Hosting](#self-hosting)
[3. Contributing Guide](#contributing)
## General Architecture
<p align="center">
<img src="./docs/archi.png" height="450px">
</p>
SimpleLogin backend consists of 2 main components:
- the `webapp` used by several clients: web UI (the dashboard), browser extension (Chrome & Firefox for now), OAuth clients (apps that integrate "Login with SimpleLogin" button) and mobile app (work in progress).
- the `email handler`: implements the email forwarding (i.e. alias receiving email) and email sending (i.e. alias sending email).
## Self hosting
### Prerequisites ### Prerequisites
@ -98,9 +44,6 @@ SimpleLogin backend consists of 2 main components:
- a domain that you can config the DNS. It could be a sub-domain. In the rest of the doc, let's say it's `mydomain.com` for the email and `app.mydomain.com` for SimpleLogin webapp. Please make sure to replace these values by your domain name whenever they appear in the doc. A trick we use is to download this README file on your computer and replace all `mydomain.com` occurrences by your domain. - a domain that you can config the DNS. It could be a sub-domain. In the rest of the doc, let's say it's `mydomain.com` for the email and `app.mydomain.com` for SimpleLogin webapp. Please make sure to replace these values by your domain name whenever they appear in the doc. A trick we use is to download this README file on your computer and replace all `mydomain.com` occurrences by your domain.
- [Optional] AWS S3, Sentry, Google/Facebook/Github developer accounts. These are necessary only if you want to activate these options.
Except for the DNS setup that is usually done on your domain registrar interface, all the below steps are to be done on your server. The commands are to run with `bash` (or any bash-compatible shell like `zsh`) being the shell. If you use other shells like `fish`, please make sure to adapt the commands. Except for the DNS setup that is usually done on your domain registrar interface, all the below steps are to be done on your server. The commands are to run with `bash` (or any bash-compatible shell like `zsh`) being the shell. If you use other shells like `fish`, please make sure to adapt the commands.
### Some utility packages ### Some utility packages
@ -108,9 +51,17 @@ Except for the DNS setup that is usually done on your domain registrar interface
These packages are used to verify the setup. Install them by: These packages are used to verify the setup. Install them by:
```bash ```bash
sudo apt install -y dnsutils sudo apt update && sudo apt install -y dnsutils
``` ```
Create a directory to store SimpleLogin data:
```bash
mkdir sl
mkdir sl/pgp # to store PGP key
mkdir sl/db # to store database
mkdir sl/upload # to store quarantine emails
```
### DKIM ### DKIM
@ -123,7 +74,7 @@ Setting up DKIM is highly recommended to reduce the chance your emails ending up
First you need to generate a private and public key for DKIM: First you need to generate a private and public key for DKIM:
```bash ```bash
openssl genrsa -out dkim.key 1024 openssl genrsa -out dkim.key -traditional 1024
openssl rsa -in dkim.key -pubout -out dkim.pub.key openssl rsa -in dkim.key -pubout -out dkim.pub.key
``` ```
@ -152,7 +103,9 @@ mydomain.com. 3600 IN MX 10 app.mydomain.com.
``` ```
#### A record #### A record
An **A record** that points `app.mydomain.com.` to your server IP. To verify, the following command An **A record** that points `app.mydomain.com.` to your server IP.
If you are using CloudFlare, we recommend to disable the "Proxy" option.
To verify, the following command
```bash ```bash
dig @1.1.1.1 app.mydomain.com a dig @1.1.1.1 app.mydomain.com a
@ -187,7 +140,7 @@ then the `PUBLIC_KEY` would be `abcdefgh`.
You can get the `PUBLIC_KEY` by running this command: You can get the `PUBLIC_KEY` by running this command:
```bash ```bash
sed "s/-----BEGIN PUBLIC KEY-----/v=DKIM1; k=rsa; p=/g" dkim.pub.key | sed 's/-----END PUBLIC KEY-----//g' |tr -d '\n' sed "s/-----BEGIN PUBLIC KEY-----/v=DKIM1; k=rsa; p=/g" $(pwd)/dkim.pub.key | sed 's/-----END PUBLIC KEY-----//g' |tr -d '\n' | awk 1
``` ```
To verify, the following command To verify, the following command
@ -208,7 +161,7 @@ Similar to DKIM, setting up SPF is highly recommended.
Add a TXT record for `mydomain.com.` with the value: Add a TXT record for `mydomain.com.` with the value:
``` ```
v=spf1 mx -all v=spf1 mx ~all
``` ```
What it means is only your server can send email with `@mydomain.com` domain. What it means is only your server can send email with `@mydomain.com` domain.
@ -251,11 +204,10 @@ Now the boring DNS stuffs are done, let's do something more fun!
If you don't already have Docker installed on your server, please follow the steps on [Docker CE for Ubuntu](https://docs.docker.com/v17.12/install/linux/docker-ce/ubuntu/) to install Docker. If you don't already have Docker installed on your server, please follow the steps on [Docker CE for Ubuntu](https://docs.docker.com/v17.12/install/linux/docker-ce/ubuntu/) to install Docker.
Tips: if you are not using `root` user and you want to run Docker without the `sudo` prefix, add your account to `docker` group with the following command. You can also install Docker using the [docker-install](https://github.com/docker/docker-install) script which is
You might need to exit and ssh again to your server for this to be taken into account.
```bash ```bash
sudo usermod -a -G docker $USER curl -fsSL https://get.docker.com | sh
``` ```
### Prepare the Docker network ### Prepare the Docker network
@ -265,8 +217,8 @@ Later, we will setup Postfix to authorize this network.
```bash ```bash
sudo docker network create -d bridge \ sudo docker network create -d bridge \
--subnet=240.0.0.0/24 \ --subnet=10.0.0.0/24 \
--gateway=240.0.0.1 \ --gateway=10.0.0.1 \
sl-network sl-network
``` ```
@ -279,12 +231,13 @@ If you already have a Postgres database in use, you can skip this section and ju
Run a Postgres Docker container as your Postgres database server. Make sure to replace `myuser` and `mypassword` with something more secret. Run a Postgres Docker container as your Postgres database server. Make sure to replace `myuser` and `mypassword` with something more secret.
```bash ```bash
sudo docker run -d \ docker run -d \
--name sl-db \ --name sl-db \
-e POSTGRES_PASSWORD=mypassword \ -e POSTGRES_PASSWORD=mypassword \
-e POSTGRES_USER=myuser \ -e POSTGRES_USER=myuser \
-e POSTGRES_DB=simplelogin \ -e POSTGRES_DB=simplelogin \
-p 5432:5432 \ -p 127.0.0.1:5432:5432 \
-v $(pwd)/sl/db:/var/lib/postgresql/data \
--restart always \ --restart always \
--network="sl-network" \ --network="sl-network" \
postgres:12.1 postgres:12.1
@ -293,7 +246,7 @@ sudo docker run -d \
To test whether the database operates correctly or not, run the following command: To test whether the database operates correctly or not, run the following command:
```bash ```bash
sudo docker exec -it sl-db psql -U myuser simplelogin docker exec -it sl-db psql -U myuser simplelogin
``` ```
you should be logged in the postgres console. Type `exit` to exit postgres console. you should be logged in the postgres console. Type `exit` to exit postgres console.
@ -308,6 +261,9 @@ sudo apt-get install -y postfix postfix-pgsql -y
Choose "Internet Site" in Postfix installation window then keep using the proposed value as *System mail name* in the next window. Choose "Internet Site" in Postfix installation window then keep using the proposed value as *System mail name* in the next window.
![](./docs/postfix-installation.png)
![](./docs/postfix-installation2.png)
Replace `/etc/postfix/main.cf` with the following content. Make sure to replace `mydomain.com` by your domain. Replace `/etc/postfix/main.cf` with the following content. Make sure to replace `mydomain.com` by your domain.
``` ```
@ -330,17 +286,19 @@ compatibility_level = 2
# TLS parameters # TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_use_tls=yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_security_level = may
smtpd_tls_security_level = may
# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for # See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client. # information on enabling SSL in the smtp client.
alias_maps = hash:/etc/aliases alias_maps = hash:/etc/aliases
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 240.0.0.0/24 mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 10.0.0.0/24
# Set your domain here # Set your domain here
mydestination =
myhostname = app.mydomain.com myhostname = app.mydomain.com
mydomain = mydomain.com mydomain = mydomain.com
myorigin = mydomain.com myorigin = mydomain.com
@ -376,8 +334,14 @@ smtpd_recipient_restrictions =
permit permit
``` ```
Check that the ssl certificates `/etc/ssl/certs/ssl-cert-snakeoil.pem` and `/etc/ssl/private/ssl-cert-snakeoil.key` exist. Depending on the linux distribution you are using they may or may not be present. If they are not, you will need to generate them with this command:
```bash
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/ssl/private/ssl-cert-snakeoil.key -out /etc/ssl/certs/ssl-cert-snakeoil.pem
```
Create the `/etc/postfix/pgsql-relay-domains.cf` file with the following content. Create the `/etc/postfix/pgsql-relay-domains.cf` file with the following content.
Make sure that the database config is correctly set and replace `mydomain.com` with your domain. Make sure that the database config is correctly set, replace `mydomain.com` with your domain, update 'myuser' and 'mypassword' with your postgres credentials.
``` ```
# postgres config # postgres config
@ -391,7 +355,7 @@ query = SELECT domain FROM custom_domain WHERE domain='%s' AND verified=true
``` ```
Create the `/etc/postfix/pgsql-transport-maps.cf` file with the following content. Create the `/etc/postfix/pgsql-transport-maps.cf` file with the following content.
Again, make sure that the database config is correctly set and replace `mydomain.com` with your domain. Again, make sure that the database config is correctly set, replace `mydomain.com` with your domain, update 'myuser' and 'mypassword' with your postgres credentials.
``` ```
# postgres config # postgres config
@ -411,14 +375,16 @@ Finally, restart Postfix
sudo systemctl restart postfix sudo systemctl restart postfix
``` ```
### Run SimpleLogin Docker containers ### Run SimpleLogin Docker containers
To run the server, you need a config file. Please have a look at [config example](example.env) for an example to create one. Some parameters are optional and are commented out by default. Some have "dummy" values, fill them up if you want to enable these features (Paddle, AWS, etc). To run SimpleLogin, you need a config file at `$(pwd)/simplelogin.env`. Below is an example that you can use right away, make sure to
Let's put your config file at `~/simplelogin.env`. Below is an example that you can use right away, make sure to replace `mydomain.com` by your domain and set `FLASK_SECRET` to a secret string. - replace `mydomain.com` by your domain,
- set `FLASK_SECRET` to a secret string,
- update 'myuser' and 'mypassword' with your database credentials used in previous step.
Make sure to update the following variables and replace these values by yours. All possible parameters can be found in [config example](example.env). Some are optional and are commented out by default.
Some have "dummy" values, fill them up if you want to enable these features (Paddle, AWS, etc).
```.env ```.env
# WebApp URL # WebApp URL
@ -437,68 +403,101 @@ EMAIL_SERVERS_WITH_PRIORITY=[(10, "app.mydomain.com.")]
# this option doesn't make sense in self-hosted. Set this variable to disable this option. # this option doesn't make sense in self-hosted. Set this variable to disable this option.
DISABLE_ALIAS_SUFFIX=1 DISABLE_ALIAS_SUFFIX=1
# If you want to use another MTA to send email, you could set the address of your MTA here
# By default, emails are sent using the the same Postfix server that receives emails
# POSTFIX_SERVER=my-postfix.com
# the DKIM private key used to compute DKIM-Signature # the DKIM private key used to compute DKIM-Signature
DKIM_PRIVATE_KEY_PATH=/dkim.key DKIM_PRIVATE_KEY_PATH=/dkim.key
# the DKIM public key used to setup custom domain DKIM
DKIM_PUBLIC_KEY_PATH=/dkim.pub.key
# DB Connection # DB Connection
DB_URI=postgresql://myuser:mypassword@sl-db:5432/simplelogin DB_URI=postgresql://myuser:mypassword@sl-db:5432/simplelogin
FLASK_SECRET=put_something_secret_here FLASK_SECRET=put_something_secret_here
GNUPGHOME=/sl/pgp
LOCAL_FILE_UPLOAD=1
POSTFIX_SERVER=10.0.0.1
``` ```
Before running the webapp, you need to prepare the database by running the migration: Before running the webapp, you need to prepare the database by running the migration:
```bash ```bash
sudo docker run --rm \ docker run --rm \
--name sl-migration \ --name sl-migration \
-v $(pwd)/sl:/sl \
-v $(pwd)/sl/upload:/code/static/upload \
-v $(pwd)/dkim.key:/dkim.key \ -v $(pwd)/dkim.key:/dkim.key \
-v $(pwd)/dkim.pub.key:/dkim.pub.key \ -v $(pwd)/dkim.pub.key:/dkim.pub.key \
-v $(pwd)/simplelogin.env:/code/.env \ -v $(pwd)/simplelogin.env:/code/.env \
--network="sl-network" \ --network="sl-network" \
simplelogin/app:1.1.0 flask db upgrade simplelogin/app:3.4.0 flask db upgrade
``` ```
This command could take a while to download the `simplelogin/app` docker image. This command could take a while to download the `simplelogin/app` docker image.
Now, it's time to run the `webapp` container! Init data
```bash ```bash
sudo docker run -d \ docker run --rm \
--name sl-app \ --name sl-init \
-v $(pwd)/sl:/sl \
-v $(pwd)/simplelogin.env:/code/.env \ -v $(pwd)/simplelogin.env:/code/.env \
-v $(pwd)/dkim.key:/dkim.key \ -v $(pwd)/dkim.key:/dkim.key \
-v $(pwd)/dkim.pub.key:/dkim.pub.key \ -v $(pwd)/dkim.pub.key:/dkim.pub.key \
-p 7777:7777 \ --network="sl-network" \
simplelogin/app:3.4.0 python init_app.py
```
Now, it's time to run the `webapp` container!
```bash
docker run -d \
--name sl-app \
-v $(pwd)/sl:/sl \
-v $(pwd)/sl/upload:/code/static/upload \
-v $(pwd)/simplelogin.env:/code/.env \
-v $(pwd)/dkim.key:/dkim.key \
-v $(pwd)/dkim.pub.key:/dkim.pub.key \
-p 127.0.0.1:7777:7777 \
--restart always \ --restart always \
--network="sl-network" \ --network="sl-network" \
simplelogin/app:1.1.0 simplelogin/app:3.4.0
``` ```
Next run the `email handler` Next run the `email handler`
```bash ```bash
sudo docker run -d \ docker run -d \
--name sl-email \ --name sl-email \
-v $(pwd)/sl:/sl \
-v $(pwd)/sl/upload:/code/static/upload \
-v $(pwd)/simplelogin.env:/code/.env \ -v $(pwd)/simplelogin.env:/code/.env \
-v $(pwd)/dkim.key:/dkim.key \ -v $(pwd)/dkim.key:/dkim.key \
-v $(pwd)/dkim.pub.key:/dkim.pub.key \ -v $(pwd)/dkim.pub.key:/dkim.pub.key \
-p 20381:20381 \ -p 127.0.0.1:20381:20381 \
--restart always \ --restart always \
--network="sl-network" \ --network="sl-network" \
simplelogin/app:1.1.0 python email_handler.py simplelogin/app:3.4.0 python email_handler.py
```
And finally the `job runner`
```bash
docker run -d \
--name sl-job-runner \
-v $(pwd)/sl:/sl \
-v $(pwd)/sl/upload:/code/static/upload \
-v $(pwd)/simplelogin.env:/code/.env \
-v $(pwd)/dkim.key:/dkim.key \
-v $(pwd)/dkim.pub.key:/dkim.pub.key \
--restart always \
--network="sl-network" \
simplelogin/app:3.4.0 python job_runner.py
``` ```
### Nginx ### Nginx
Install Nginx Install Nginx and make sure to replace `mydomain.com` by your domain
```bash ```bash
sudo apt-get install -y nginx sudo apt-get install -y nginx
@ -506,510 +505,66 @@ sudo apt-get install -y nginx
Then, create `/etc/nginx/sites-enabled/simplelogin` with the following lines: Then, create `/etc/nginx/sites-enabled/simplelogin` with the following lines:
``` ```nginx
server { server {
server_name app.mydomain.com; server_name app.mydomain.com;
location / { location / {
proxy_pass http://localhost:7777; proxy_pass http://localhost:7777;
proxy_set_header Host $host;
} }
} }
``` ```
Note: If `/etc/nginx/sites-enabled/default` exists, delete it or certbot will fail due to the conflict. The `simplelogin` file should be the only file in `sites-enabled`.
Reload Nginx with the command below Reload Nginx with the command below
```bash ```bash
sudo systemctl reload nginx sudo systemctl reload nginx
``` ```
At this step, you should also setup the SSL for Nginx. [Certbot](https://certbot.eff.org/lets-encrypt/ubuntuxenial-nginx) can be a good option if you want a free SSL certificate. At this step, you should also setup the SSL for Nginx. [Here's our guide how](./docs/ssl.md).
### Enjoy! ### Enjoy!
If all of the above steps are successful, open http://app.mydomain.com/ and create your first account! If all the above steps are successful, open http://app.mydomain.com/ and create your first account!
By default, new accounts are not premium so don't have unlimited alias. To make your account premium, By default, new accounts are not premium so don't have unlimited alias. To make your account premium,
please go to the database, table "users" and set "lifetime" column to "1" or "TRUE". please go to the database, table "users" and set "lifetime" column to "1" or "TRUE":
```
docker exec -it sl-db psql -U myuser simplelogin
UPDATE users SET lifetime = TRUE;
exit
```
Once you've created all your desired login accounts, add these lines to `/simplelogin.env` to disable further registrations:
```.env
DISABLE_REGISTRATION=1
DISABLE_ONBOARDING=true
```
Then restart the web app to apply: `docker restart sl-app`
### Donations Welcome
You don't have to pay anything to SimpleLogin to use all its features. You don't have to pay anything to SimpleLogin to use all its features.
You could make a donation to SimpleLogin on our Patreon page at https://www.patreon.com/simplelogin if you wish though. If you like the project, you can make a donation on our Open Collective page at https://opencollective.com/simplelogin
### Misc ### Misc
The above self-hosting instructions correspond to a freshly Ubuntu server and doesn't cover all possible server configuration. The above self-hosting instructions correspond to a freshly Ubuntu server and doesn't cover all possible server configuration.
Below are pointers to different topics: Below are pointers to different topics:
- [Troubleshooting](docs/troubleshooting.md)
- [Enable SSL](docs/ssl.md)
- [UFW - uncomplicated firewall](docs/ufw.md) - [UFW - uncomplicated firewall](docs/ufw.md)
- [SES - Amazon Simple Email Service](docs/ses.md) - [SES - Amazon Simple Email Service](docs/ses.md)
- [Upgrade existing SimpleLogin installation](docs/upgrade.md) - [Upgrade existing SimpleLogin installation](docs/upgrade.md)
- [Enforce SPF](docs/enforce-spf.md)
## Contributing - [Postfix TLS](docs/postfix-tls.md)
All work on SimpleLogin happens directly on GitHub.
### Run code locally
The project uses Python 3.7+ and Node v10. First, install all dependencies by running the following command. Feel free to use `virtualenv` or similar tools to isolate development environment.
```bash
pip3 install -r requirements.txt
```
You also need to install `gpg`, on Mac it can be done with:
```bash
brew install gnupg
```
Then make sure all tests pass
```bash
pytest
```
Install npm packages
```bash
cd static && npm install
```
To run the code locally, please create a local setting file based on `example.env`:
```
cp example.env .env
```
Make sure to uncomment the `RESET_DB=true` to create the database locally.
Feel free to custom your `.env` file, it would be your default setting when developing locally. This file is ignored by git.
You don't need all the parameters, for example, if you don't update images to s3, then
`BUCKET`, `AWS_ACCESS_KEY_ID` can be empty or if you don't use login with Github locally, `GITHUB_CLIENT_ID` doesn't have to be filled. The `example.env` file contains minimal requirement so that if you run:
```
python3 server.py
```
then open http://localhost:7777, you should be able to login with the following account
```
john@wick.com / password
```
### API
SimpleLogin current API clients are Chrome/Firefox/Safari extension and mobile (iOS/Android) app.
These clients rely on `API Code` for authentication.
Once the `Api Code` is obtained, either via user entering it (in Browser extension case) or by logging in (in Mobile case),
the client includes the `api code` in `Authentication` header in almost all requests.
For some endpoints, the `hostname` should be passed in query string. `hostname` is the the URL hostname (cf https://en.wikipedia.org/wiki/URL), for ex if URL is http://www.example.com/index.html then the hostname is `www.example.com`. This information is important to know where an alias is used in order to suggest user the same alias if they want to create on alias on the same website in the future.
If error, the API returns 4** with body containing the error message, for example:
```json
{
"error": "request body cannot be empty"
}
```
The error message could be displayed to user as-is, for example for when user exceeds their alias quota.
Some errors should be fixed during development however: for example error like `request body cannot be empty` is there to catch development error and should never be shown to user.
All following endpoint return `401` status code if the API Key is incorrect.
#### GET /api/user_info
Given the API Key, return user name and whether user is premium.
This endpoint could be used to validate the api key.
Input:
- `Authentication` header that contains the api key
Output: if api key is correct, return a json with user name and whether user is premium, for example:
```json
{
"name": "John Wick",
"is_premium": false
}
```
If api key is incorrect, return 401.
#### GET /api/v2/alias/options
User alias info and suggestion. Used by the first extension screen when user opens the extension.
Input:
- `Authentication` header that contains the api key
- (Optional but recommended) `hostname` passed in query string.
Output: a json with the following field:
- can_create: boolean. Whether user can create new alias
- suffixes: list of string. List of alias `suffix` that user can use. If user doesn't have custom domain, this list has a single element which is the alias default domain (simplelogin.co).
- prefix_suggestion: string. Suggestion for the `alias prefix`. Usually this is the website name extracted from `hostname`. If no `hostname`, then the `prefix_suggestion` is empty.
- existing: list of string. List of existing alias.
- recommendation: optional field, dictionary. If an alias is already used for this website, the recommendation will be returned. There are 2 subfields in `recommendation`: `alias` which is the recommended alias and `hostname` is the website on which this alias is used before.
For ex:
```json
{
"can_create": true,
"existing": [
"my-first-alias.meo@sl.local",
"e1.cat@sl.local",
"e2.chat@sl.local",
"e3.cat@sl.local"
],
"prefix_suggestion": "test",
"recommendation": {
"alias": "e1.cat@sl.local",
"hostname": "www.test.com"
},
"suffixes": [
"@very-long-domain.com.net.org",
"@ab.cd",
".cat@sl.local"
]
}
```
#### POST /api/alias/custom/new
Create a new custom alias.
Input:
- `Authentication` header that contains the api key
- (Optional but recommended) `hostname` passed in query string
- Request Message Body in json (`Content-Type` is `application/json`)
- alias_prefix: string. The first part of the alias that user can choose.
- alias_suffix: should be one of the suffixes returned in the `GET /api/v2/alias/options` endpoint.
- (Optional) note: alias note
Output:
If success, 201 with the new alias, for example
```json
{
"alias": "www_groupon_com@my_domain.com"
}
```
#### POST /api/alias/random/new
Create a new random alias.
Input:
- `Authentication` header that contains the api key
- (Optional but recommended) `hostname` passed in query string
- (Optional) mode: either `uuid` or `word`. By default, use the user setting when creating new random alias.
- Request Message Body in json (`Content-Type` is `application/json`)
- (Optional) note: alias note
Output:
If success, 201 with the new alias, for example
```json
{
"alias": "www_groupon_com@my_domain.com"
}
```
#### POST /api/auth/login
Input:
- email
- password
- device: device name. Used to create the API Key. Should be humanly readable so user can manage later on the "API Key" page.
Output:
- name: user name, could be an empty string
- mfa_enabled: boolean
- mfa_key: only useful when user enables MFA. In this case, user needs to enter their OTP token in order to login.
- api_key: if MFA is not enabled, the `api key` is returned right away.
The `api_key` is used in all subsequent requests. It's empty if MFA is enabled.
If user hasn't enabled MFA, `mfa_key` is empty.
#### POST /api/auth/mfa
Input:
- mfa_token: OTP token that user enters
- mfa_key: MFA key obtained in previous auth request, e.g. /api/auth/login
- device: the device name, used to create an ApiKey associated with this device
Output:
- name: user name, could be an empty string
- api_key: if MFA is not enabled, the `api key` is returned right away.
The `api_key` is used in all subsequent requests. It's empty if MFA is enabled.
If user hasn't enabled MFA, `mfa_key` is empty.
#### POST /api/auth/facebook
Input:
- facebook_token: Facebook access token
- device: device name. Used to create the API Key. Should be humanly readable so user can manage later on the "API Key" page.
Output: Same output as for `/api/auth/login` endpoint
#### POST /api/auth/google
Input:
- google_token: Google access token
- device: device name. Used to create the API Key. Should be humanly readable so user can manage later on the "API Key" page.
Output: Same output as for `/api/auth/login` endpoint
#### POST /api/auth/register
Input:
- email
- password
Output: 200 means user is going to receive an email that contains an *activation code*. User needs to enter this code to confirm their account -> next endpoint.
#### POST /api/auth/activate
Input:
- email
- code: the activation code
Output:
- 200: account is activated. User can login now
- 400: wrong email, code
- 410: wrong code too many times. User needs to ask for an reactivation -> next endpoint
#### POST /api/auth/reactivate
Input:
- email
Output:
- 200: user is going to receive an email that contains the activation code.
#### GET /api/aliases
Get user aliases.
Input:
- `Authentication` header that contains the api key
- `page_id` used for the pagination. The endpoint returns maximum 20 aliases for each page. `page_id` starts at 0.
Output:
If success, 200 with the list of aliases, for example:
```json
{
"aliases": [
{
"creation_date": "2020-02-04 16:23:02+00:00",
"creation_timestamp": 1580833382,
"email": "e3@.alo@sl.local",
"id": 4,
"nb_block": 0,
"nb_forward": 0,
"nb_reply": 0,
"enabled": true,
"note": "This is a note"
},
{
"creation_date": "2020-02-04 16:23:02+00:00",
"creation_timestamp": 1580833382,
"email": "e2@.meo@sl.local",
"id": 3,
"nb_block": 0,
"nb_forward": 0,
"nb_reply": 0,
"enabled": false,
"note": null
}
]
}
```
#### DELETE /api/aliases/:alias_id
Delete an alias
Input:
- `Authentication` header that contains the api key
- `alias_id` in url.
Output:
If success, 200.
```json
{
"deleted": true
}
```
#### POST /api/aliases/:alias_id/toggle
Enable/disable alias
Input:
- `Authentication` header that contains the api key
- `alias_id` in url.
Output:
If success, 200 along with the new alias status:
```json
{
"enabled": false
}
```
#### GET /api/aliases/:alias_id/activities
Get activities for a given alias.
Input:
- `Authentication` header that contains the api key
- `alias_id`: the alias id, passed in url.
- `page_id` used in request query (`?page_id=0`). The endpoint returns maximum 20 aliases for each page. `page_id` starts at 0.
Output:
If success, 200 with the list of activities, for example:
```json
{
"activities": [
{
"action": "reply",
"from": "yes_meo_chat@sl.local",
"timestamp": 1580903760,
"to": "marketing@example.com"
},
{
"action": "reply",
"from": "yes_meo_chat@sl.local",
"timestamp": 1580903760,
"to": "marketing@example.com"
}
]
}
```
### Database migration
The database migration is handled by `alembic`
Whenever the model changes, a new migration has to be created
Set the database connection to use a current database (i.e. the one without the model changes you just made), for example, if you have a staging config at `~/config/simplelogin/staging.env`, you can do:
```bash
ln -sf ~/config/simplelogin/staging.env .env
```
Generate the migration script and make sure to review it before committing it. Sometimes (very rarely though), the migration generation can go wrong.
```bash
flask db migrate
```
In local the database creation in Sqlite doesn't use migration and uses directly `db.create_all()` (cf `fake_data()` method). This is because Sqlite doesn't handle well the migration. As sqlite is only used during development, the database is deleted and re-populated at each run.
### Code structure
The repo consists of the three following entry points:
- wsgi.py and server.py: the webapp.
- email_handler.py: the email handler.
- cron.py: the cronjob.
Here are the small sum-ups of the directory structures and their roles:
- app/: main Flask app. It is structured into different packages representing different features like oauth, api, dashboard, etc.
- local_data/: contains files to facilitate the local development. They are replaced during the deployment.
- migrations/: generated by flask-migrate. Edit these files will be only edited when you spot (very rare) errors on the database migration files.
- static/: files available at `/static` url.
- templates/: contains both html and email templates.
- tests/: tests. We don't really distinguish unit, functional or integration test. A test is simply here to make sure a feature works correctly.
The code is formatted using https://github.com/psf/black, to format the code, simply run
```
black .
```
### OAuth flow
SL currently supports code and implicit flow.
#### Code flow
To trigger the code flow locally, you can go to the following url after running `python server.py`:
```
http://localhost:7777/oauth/authorize?client_id=client-id&state=123456&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A7000%2Fcallback&state=random_string
```
You should see there the authorization page where user is asked for permission to share their data. Once user approves, user is redirected to this url with an `authorization code`: `http://localhost:7000/callback?state=123456&code=the_code`
Next, exchange the code to get the token with `{code}` replaced by the code obtained in previous step. The `http` tool used here is https://httpie.org
```
http -f -a client-id:client-secret http://localhost:7777/oauth/token grant_type=authorization_code code={code}
```
This should return an `access token` that allows to get user info via the following command. Again, `http` tool is used.
```
http http://localhost:7777/oauth/user_info 'Authorization:Bearer {token}'
```
#### Implicit flow
Similar to code flow, except for the the `access token` which we we get back with the redirection.
For implicit flow, the url is
```
http://localhost:7777/oauth/authorize?client_id=client-id&state=123456&response_type=token&redirect_uri=http%3A%2F%2Flocalhost%3A7000%2Fcallback&state=random_string
```
#### OpenID and OAuth2 response_type & scope
According to the sharing web blog titled [Diagrams of All The OpenID Connect Flows](https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660), we should pay attention to:
- `response_type` can be either `code, token, id_token` or any combination of those attributes.
- `scope` might contain `openid`
Below are the potential combinations that are taken into account in SL until now:
```
response_type=code
scope:
with `openid` in scope, return `id_token` at /token: OK
without: OK
response_type=token
scope:
with and without `openid`, nothing to do: OK
response_type=id_token
return `id_token` in /authorization endpoint
response_type=id_token token
return `id_token` in addition to `access_token` in /authorization endpoint
response_type=id_token code
return `id_token` in addition to `authorization_code` in /authorization endpoint
```
## ❤️ Contributors ## ❤️ Contributors
@ -1022,5 +577,8 @@ Thanks go to these wonderful people:
<td align="center"><a href="https://github.com/NinhDinh"><img src="https://avatars2.githubusercontent.com/u/1419742?s=460&v=4" width="100px;" alt="Ninh Dinh"/><br /><sub><b>Ninh Dinh</b></sub></a><br /></td> <td align="center"><a href="https://github.com/NinhDinh"><img src="https://avatars2.githubusercontent.com/u/1419742?s=460&v=4" width="100px;" alt="Ninh Dinh"/><br /><sub><b>Ninh Dinh</b></sub></a><br /></td>
<td align="center"><a href="https://github.com/ntung"><img src="https://avatars1.githubusercontent.com/u/663341?s=460&v=4" width="100px;" alt="Tung Nguyen V. N."/><br /><sub><b>Tung Nguyen V. N.</b></sub></a><br /></td> <td align="center"><a href="https://github.com/ntung"><img src="https://avatars1.githubusercontent.com/u/663341?s=460&v=4" width="100px;" alt="Tung Nguyen V. N."/><br /><sub><b>Tung Nguyen V. N.</b></sub></a><br /></td>
<td align="center"><a href="https://www.linkedin.com/in/nguyenkims/"><img src="https://simplelogin.io/about/me.jpeg" width="100px;" alt="Son Nguyen Kim"/><br /><sub><b>Son Nguyen Kim</b></sub></a><br /></td> <td align="center"><a href="https://www.linkedin.com/in/nguyenkims/"><img src="https://simplelogin.io/about/me.jpeg" width="100px;" alt="Son Nguyen Kim"/><br /><sub><b>Son Nguyen Kim</b></sub></a><br /></td>
<td align="center"><a href="https://github.com/developStorm"><img src="https://avatars1.githubusercontent.com/u/59678453?s=460&u=3813d29a125b3edeb44019234672b704f7b9b76a&v=4" width="100px;" alt="Raymond Nook"/><br /><sub><b>Raymond Nook</b></sub></a><br /></td>
<td align="center"><a href="https://github.com/SibrenVasse"><img src="https://avatars1.githubusercontent.com/u/5833571?s=460&u=78aea62ffc215885a0319437fc629a7596ddea31&v=4" width="100px;" alt="Sibren Vasse"/><br /><sub><b>Sibren Vasse</b></sub></a><br /></td>
<td align="center"><a href="https://github.com/TheLastProject"><img src="https://avatars.githubusercontent.com/u/1885159?s=460&u=ebeeb346c4083c0d493a134f4774f925d3437f98&v=4" width="100px;" alt="Sylvia van Os"/><br /><sub><b>Sylvia van Os</b></sub></a><br /></td>
</tr> </tr>
</table> </table>

14
SECURITY.md Normal file
View File

@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
We only add security updates to the latest MAJOR.MINOR version of the project. No security updates are backported to previous versions.
If you want be up to date on security patches, make sure your SimpleLogin image is up to date.
## Reporting a Vulnerability
If you've found a security vulnerability, you can disclose it responsibly by sending a summary to security@simplelogin.io.
We will review the potential threat and fix it as fast as we can.
We are incredibly thankful for people who disclose vulnerabilities, unfortunately we do not have a bounty program in place yet.

83
alembic.ini Normal file
View File

@ -0,0 +1,83 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
file_template = %%(year)d_%%(month).2d%%(day).2d%%(hour).2d_%%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black
# black.type=console_scripts
# black.entrypoint=black
# black.options=-l 79
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

316
app/account_linking.py Normal file
View File

@ -0,0 +1,316 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from arrow import Arrow
from newrelic import agent
from sqlalchemy import or_
from app.db import Session
from app.email_utils import send_welcome_email
from app.utils import sanitize_email, canonicalize_email
from app.errors import (
AccountAlreadyLinkedToAnotherPartnerException,
AccountIsUsingAliasAsEmail,
AccountAlreadyLinkedToAnotherUserException,
)
from app.log import LOG
from app.models import (
PartnerSubscription,
Partner,
PartnerUser,
User,
Alias,
)
from app.utils import random_string
class SLPlanType(Enum):
Free = 1
Premium = 2
@dataclass
class SLPlan:
type: SLPlanType
expiration: Optional[Arrow]
@dataclass
class PartnerLinkRequest:
name: str
email: str
external_user_id: str
plan: SLPlan
from_partner: bool
@dataclass
class LinkResult:
user: User
strategy: str
def set_plan_for_partner_user(partner_user: PartnerUser, plan: SLPlan):
sub = PartnerSubscription.get_by(partner_user_id=partner_user.id)
if plan.type == SLPlanType.Free:
if sub is not None:
LOG.i(
f"Deleting partner_subscription [user_id={partner_user.user_id}] [partner_id={partner_user.partner_id}]"
)
PartnerSubscription.delete(sub.id)
agent.record_custom_event("PlanChange", {"plan": "free"})
else:
if sub is None:
LOG.i(
f"Creating partner_subscription [user_id={partner_user.user_id}] [partner_id={partner_user.partner_id}]"
)
PartnerSubscription.create(
partner_user_id=partner_user.id,
end_at=plan.expiration,
)
agent.record_custom_event("PlanChange", {"plan": "premium", "type": "new"})
else:
if sub.end_at != plan.expiration:
LOG.i(
f"Updating partner_subscription [user_id={partner_user.user_id}] [partner_id={partner_user.partner_id}]"
)
agent.record_custom_event(
"PlanChange", {"plan": "premium", "type": "extension"}
)
sub.end_at = plan.expiration
Session.commit()
def set_plan_for_user(user: User, plan: SLPlan, partner: Partner):
partner_user = PartnerUser.get_by(partner_id=partner.id, user_id=user.id)
if partner_user is None:
return
return set_plan_for_partner_user(partner_user, plan)
def ensure_partner_user_exists_for_user(
link_request: PartnerLinkRequest, sl_user: User, partner: Partner
) -> PartnerUser:
# Find partner_user by user_id
res = PartnerUser.get_by(user_id=sl_user.id)
if res and res.partner_id != partner.id:
raise AccountAlreadyLinkedToAnotherPartnerException()
if not res:
res = PartnerUser.create(
user_id=sl_user.id,
partner_id=partner.id,
partner_email=link_request.email,
external_user_id=link_request.external_user_id,
)
Session.commit()
LOG.i(
f"Created new partner_user for partner:{partner.id} user:{sl_user.id} external_user_id:{link_request.external_user_id}. PartnerUser.id is {res.id}"
)
return res
class ClientMergeStrategy(ABC):
def __init__(
self,
link_request: PartnerLinkRequest,
user: Optional[User],
partner: Partner,
):
if self.__class__ == ClientMergeStrategy:
raise RuntimeError("Cannot directly instantiate a ClientMergeStrategy")
self.link_request = link_request
self.user = user
self.partner = partner
@abstractmethod
def process(self) -> LinkResult:
pass
class NewUserStrategy(ClientMergeStrategy):
def process(self) -> LinkResult:
# Will create a new SL User with a random password
canonical_email = canonicalize_email(self.link_request.email)
new_user = User.create(
email=canonical_email,
name=self.link_request.name,
password=random_string(20),
activated=True,
from_partner=self.link_request.from_partner,
)
partner_user = PartnerUser.create(
user_id=new_user.id,
partner_id=self.partner.id,
external_user_id=self.link_request.external_user_id,
partner_email=self.link_request.email,
)
LOG.i(
f"Created new user for login request for partner:{self.partner.id} external_user_id:{self.link_request.external_user_id}. New user {new_user.id} partner_user:{partner_user.id}"
)
set_plan_for_partner_user(
partner_user,
self.link_request.plan,
)
Session.commit()
if not new_user.created_by_partner:
send_welcome_email(new_user)
agent.record_custom_event("PartnerUserCreation", {"partner": self.partner.name})
return LinkResult(
user=new_user,
strategy=self.__class__.__name__,
)
class ExistingUnlinkedUserStrategy(ClientMergeStrategy):
def process(self) -> LinkResult:
# IF it was scheduled to be deleted. Unschedule it.
self.user.delete_on = None
partner_user = ensure_partner_user_exists_for_user(
self.link_request, self.user, self.partner
)
set_plan_for_partner_user(partner_user, self.link_request.plan)
return LinkResult(
user=self.user,
strategy=self.__class__.__name__,
)
class LinkedWithAnotherPartnerUserStrategy(ClientMergeStrategy):
def process(self) -> LinkResult:
raise AccountAlreadyLinkedToAnotherUserException()
def get_login_strategy(
link_request: PartnerLinkRequest, user: Optional[User], partner: Partner
) -> ClientMergeStrategy:
if user is None:
# We couldn't find any SimpleLogin user with the requested e-mail
return NewUserStrategy(link_request, user, partner)
# Check if user is already linked with another partner_user
other_partner_user = PartnerUser.get_by(partner_id=partner.id, user_id=user.id)
if other_partner_user is not None:
return LinkedWithAnotherPartnerUserStrategy(link_request, user, partner)
# There is a SimpleLogin user with the partner_user's e-mail
return ExistingUnlinkedUserStrategy(link_request, user, partner)
def check_alias(email: str) -> bool:
alias = Alias.get_by(email=email)
if alias is not None:
raise AccountIsUsingAliasAsEmail()
def process_login_case(
link_request: PartnerLinkRequest, partner: Partner
) -> LinkResult:
# Sanitize email just in case
link_request.email = sanitize_email(link_request.email)
# Try to find a SimpleLogin user registered with that partner user id
partner_user = PartnerUser.get_by(
partner_id=partner.id, external_user_id=link_request.external_user_id
)
if partner_user is None:
canonical_email = canonicalize_email(link_request.email)
# We didn't find any SimpleLogin user registered with that partner user id
# Make sure they aren't using an alias as their link email
check_alias(link_request.email)
check_alias(canonical_email)
# Try to find it using the partner's e-mail address
users = User.filter(
or_(User.email == link_request.email, User.email == canonical_email)
).all()
if len(users) > 1:
user = [user for user in users if user.email == canonical_email][0]
elif len(users) == 1:
user = users[0]
else:
user = None
return get_login_strategy(link_request, user, partner).process()
else:
# We found the SL user registered with that partner user id
# We're done
set_plan_for_partner_user(partner_user, link_request.plan)
# It's the same user. No need to do anything
return LinkResult(
user=partner_user.user,
strategy="Link",
)
def link_user(
link_request: PartnerLinkRequest, current_user: User, partner: Partner
) -> LinkResult:
# Sanitize email just in case
link_request.email = sanitize_email(link_request.email)
# If it was scheduled to be deleted. Unschedule it.
current_user.delete_on = None
partner_user = ensure_partner_user_exists_for_user(
link_request, current_user, partner
)
set_plan_for_partner_user(partner_user, link_request.plan)
agent.record_custom_event("AccountLinked", {"partner": partner.name})
Session.commit()
return LinkResult(
user=current_user,
strategy="Link",
)
def switch_already_linked_user(
link_request: PartnerLinkRequest, partner_user: PartnerUser, current_user: User
):
# Find if the user has another link and unlink it
other_partner_user = PartnerUser.get_by(
user_id=current_user.id,
partner_id=partner_user.partner_id,
)
if other_partner_user is not None:
LOG.i(
f"Deleting previous partner_user:{other_partner_user.id} from user:{current_user.id}"
)
PartnerUser.delete(other_partner_user.id)
LOG.i(f"Linking partner_user:{partner_user.id} to user:{current_user.id}")
# Link this partner_user to the current user
partner_user.user_id = current_user.id
# Set plan
set_plan_for_partner_user(partner_user, link_request.plan)
Session.commit()
return LinkResult(
user=current_user,
strategy="Link",
)
def process_link_case(
link_request: PartnerLinkRequest,
current_user: User,
partner: Partner,
) -> LinkResult:
# Sanitize email just in case
link_request.email = sanitize_email(link_request.email)
# Try to find a SimpleLogin user linked with this Partner account
partner_user = PartnerUser.get_by(
partner_id=partner.id, external_user_id=link_request.external_user_id
)
if partner_user is None:
# There is no SL user linked with the partner. Proceed with linking
return link_user(link_request, current_user, partner)
# There is a SL user registered with the partner. Check if is the current one
if partner_user.user_id == current_user.id:
# Update plan
set_plan_for_partner_user(partner_user, link_request.plan)
# It's the same user. No need to do anything
return LinkResult(
user=current_user,
strategy="Link",
)
else:
return switch_already_linked_user(link_request, partner_user, current_user)

View File

@ -1,16 +1,109 @@
from flask import redirect, url_for, request from __future__ import annotations
from typing import Optional
import arrow
import sqlalchemy
from flask_admin import BaseView
from flask_admin.form import SecureForm
from flask_admin.model.template import EndpointLinkRowAction
from markupsafe import Markup
from app import models, s3
from flask import redirect, url_for, request, flash, Response
from flask_admin import expose, AdminIndexView from flask_admin import expose, AdminIndexView
from flask_admin.actions import action
from flask_admin.contrib import sqla from flask_admin.contrib import sqla
from flask_login import current_user from flask_login import current_user
from app.db import Session
from app.models import (
User,
ManualSubscription,
Fido,
Subscription,
AppleSubscription,
AdminAuditLog,
AuditLogActionEnum,
ProviderComplaintState,
Phase,
ProviderComplaint,
Alias,
Newsletter,
PADDLE_SUBSCRIPTION_GRACE_DAYS,
Mailbox,
DeletedAlias,
DomainDeletedAlias,
PartnerUser,
)
from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address
def _admin_action_formatter(view, context, model, name):
action_name = AuditLogActionEnum.get_name(model.action)
return "{} ({})".format(action_name, model.action)
def _admin_date_formatter(view, context, model, name):
return model.created_at.format()
def _user_upgrade_channel_formatter(view, context, model, name):
return Markup(model.upgrade_channel)
class SLModelView(sqla.ModelView): class SLModelView(sqla.ModelView):
column_default_sort = ("id", True)
column_display_pk = True
page_size = 100
can_edit = False
can_create = False
can_delete = False
edit_modal = True
def is_accessible(self): def is_accessible(self):
return current_user.is_authenticated and current_user.is_admin return current_user.is_authenticated and current_user.is_admin
def inaccessible_callback(self, name, **kwargs): def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access # redirect to login page if user doesn't have access
return redirect(url_for("auth.login", next=request.url)) flash("You don't have access to the admin page", "error")
return redirect(url_for("dashboard.index", next=request.url))
def on_model_change(self, form, model, is_created):
changes = {}
for attr in sqlalchemy.inspect(model).attrs:
if attr.history.has_changes() and attr.key not in (
"created_at",
"updated_at",
):
value = attr.value
# If it's a model reference, get the source id
if issubclass(type(value), models.Base):
value = value.id
# otherwise, if its a generic object stringify it
if issubclass(type(value), object):
value = str(value)
changes[attr.key] = value
auditAction = (
AuditLogActionEnum.create_object
if is_created
else AuditLogActionEnum.update_object
)
AdminAuditLog.create(
admin_user_id=current_user.id,
model=model.__class__.__name__,
model_id=model.id,
action=auditAction.value,
data=changes,
)
def on_model_delete(self, model):
AdminAuditLog.create(
admin_user_id=current_user.id,
model=model.__class__.__name__,
model_id=model.id,
action=AuditLogActionEnum.delete_object.value,
)
class SLAdminIndexView(AdminIndexView): class SLAdminIndexView(AdminIndexView):
@ -19,4 +112,714 @@ class SLAdminIndexView(AdminIndexView):
if not current_user.is_authenticated or not current_user.is_admin: if not current_user.is_authenticated or not current_user.is_admin:
return redirect(url_for("auth.login", next=request.url)) return redirect(url_for("auth.login", next=request.url))
return super(SLAdminIndexView, self).index() return redirect("/admin/user")
class UserAdmin(SLModelView):
form_base_class = SecureForm
column_searchable_list = ["email", "id"]
column_exclude_list = [
"salt",
"password",
"otp_secret",
"last_otp",
"fido_uuid",
"profile_picture",
]
can_edit = False
def scaffold_list_columns(self):
ret = super().scaffold_list_columns()
ret.insert(0, "upgrade_channel")
return ret
column_formatters = {
"upgrade_channel": _user_upgrade_channel_formatter,
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
}
@action(
"disable_user",
"Disable user",
"Are you sure you want to disable the selected users?",
)
def action_disable_user(self, ids):
for user in User.filter(User.id.in_(ids)):
user.disabled = True
flash(f"Disabled user {user.id}")
AdminAuditLog.disable_user(current_user.id, user.id)
Session.commit()
@action(
"enable_user",
"Enable user",
"Are you sure you want to enable the selected users?",
)
def action_enable_user(self, ids):
for user in User.filter(User.id.in_(ids)):
user.disabled = False
flash(f"Enabled user {user.id}")
AdminAuditLog.enable_user(current_user.id, user.id)
Session.commit()
@action(
"education_upgrade",
"Education upgrade",
"Are you sure you want to edu-upgrade selected users?",
)
def action_edu_upgrade(self, ids):
manual_upgrade("Edu", ids, is_giveaway=True)
@action(
"charity_org_upgrade",
"Charity Organization upgrade",
"Are you sure you want to upgrade selected users using the Charity organization program?",
)
def action_charity_org_upgrade(self, ids):
manual_upgrade("Charity Organization", ids, is_giveaway=True)
@action(
"journalist_upgrade",
"Journalist upgrade",
"Are you sure you want to upgrade selected users using the Journalist program?",
)
def action_journalist_upgrade(self, ids):
manual_upgrade("Journalist", ids, is_giveaway=True)
@action(
"cash_upgrade",
"Cash upgrade",
"Are you sure you want to cash-upgrade selected users?",
)
def action_cash_upgrade(self, ids):
manual_upgrade("Cash", ids, is_giveaway=False)
@action(
"crypto_upgrade",
"Crypto upgrade",
"Are you sure you want to crypto-upgrade selected users?",
)
def action_monero_upgrade(self, ids):
manual_upgrade("Crypto", ids, is_giveaway=False)
@action(
"adhoc_upgrade",
"Adhoc upgrade - for exceptional case",
"Are you sure you want to crypto-upgrade selected users?",
)
def action_adhoc_upgrade(self, ids):
manual_upgrade("Adhoc", ids, is_giveaway=False)
@action(
"extend_trial_1w",
"Extend trial for 1 week more",
"Extend trial for 1 week more?",
)
def extend_trial_1w(self, ids):
for user in User.filter(User.id.in_(ids)):
if user.trial_end and user.trial_end > arrow.now():
user.trial_end = user.trial_end.shift(weeks=1)
else:
user.trial_end = arrow.now().shift(weeks=1)
flash(f"Extend trial for {user} to {user.trial_end}", "success")
AdminAuditLog.extend_trial(
current_user.id, user.id, user.trial_end, "1 week"
)
Session.commit()
@action(
"remove trial",
"Stop trial period",
"Remove trial for this user?",
)
def stop_trial(self, ids):
for user in User.filter(User.id.in_(ids)):
user.trial_end = None
flash(f"Stopped trial for {user}", "success")
AdminAuditLog.stop_trial(current_user.id, user.id)
Session.commit()
@action(
"disable_otp_fido",
"Disable OTP & FIDO",
"Disable OTP & FIDO?",
)
def disable_otp_fido(self, ids):
for user in User.filter(User.id.in_(ids)):
user_had_otp = user.enable_otp
if user.enable_otp:
user.enable_otp = False
flash(f"Disable OTP for {user}", "info")
user_had_fido = user.fido_uuid is not None
if user.fido_uuid:
Fido.filter_by(uuid=user.fido_uuid).delete()
user.fido_uuid = None
flash(f"Disable FIDO for {user}", "info")
AdminAuditLog.disable_otp_fido(
current_user.id, user.id, user_had_otp, user_had_fido
)
Session.commit()
@action(
"stop_paddle_sub",
"Stop user Paddle subscription",
"This will stop the current user Paddle subscription so if user doesn't have Proton sub, they will lose all SL benefits immediately",
)
def stop_paddle_sub(self, ids):
for user in User.filter(User.id.in_(ids)):
sub: Subscription = user.get_paddle_subscription()
if not sub:
flash(f"No Paddle sub for {user}", "warning")
continue
flash(f"{user} sub will end now, instead of {sub.next_bill_date}", "info")
sub.next_bill_date = (
arrow.now().shift(days=-PADDLE_SUBSCRIPTION_GRACE_DAYS).date()
)
Session.commit()
@action(
"clear_delete_on",
"Remove scheduled deletion of user",
"This will remove the scheduled deletion for this users",
)
def clean_delete_on(self, ids):
for user in User.filter(User.id.in_(ids)):
user.delete_on = None
Session.commit()
# @action(
# "login_as",
# "Login as this user",
# "Login as this user?",
# )
# def login_as(self, ids):
# if len(ids) != 1:
# flash("only 1 user can be selected", "error")
# return
#
# for user in User.filter(User.id.in_(ids)):
# AdminAuditLog.logged_as_user(current_user.id, user.id)
# login_user(user)
# flash(f"Login as user {user}", "success")
# return redirect("/")
def manual_upgrade(way: str, ids: [int], is_giveaway: bool):
for user in User.filter(User.id.in_(ids)).all():
if user.lifetime:
flash(f"user {user} already has a lifetime license", "warning")
continue
sub: Subscription = user.get_paddle_subscription()
if sub and not sub.cancelled:
flash(
f"user {user} already has a Paddle license, they have to cancel it first",
"warning",
)
continue
apple_sub: AppleSubscription = AppleSubscription.get_by(user_id=user.id)
if apple_sub and apple_sub.is_valid():
flash(
f"user {user} already has a Apple subscription, they have to cancel it first",
"warning",
)
continue
AdminAuditLog.create_manual_upgrade(current_user.id, way, user.id, is_giveaway)
manual_sub: ManualSubscription = ManualSubscription.get_by(user_id=user.id)
if manual_sub:
# renew existing subscription
if manual_sub.end_at > arrow.now():
manual_sub.end_at = manual_sub.end_at.shift(years=1)
else:
manual_sub.end_at = arrow.now().shift(years=1, days=1)
flash(f"Subscription extended to {manual_sub.end_at.humanize()}", "success")
continue
ManualSubscription.create(
user_id=user.id,
end_at=arrow.now().shift(years=1, days=1),
comment=way,
is_giveaway=is_giveaway,
)
flash(f"New {way} manual subscription for {user} is created", "success")
Session.commit()
class EmailLogAdmin(SLModelView):
form_base_class = SecureForm
column_searchable_list = ["id"]
column_filters = ["id", "user.email", "mailbox.email", "contact.website_email"]
can_edit = False
can_create = False
column_formatters = {
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
}
class AliasAdmin(SLModelView):
form_base_class = SecureForm
column_searchable_list = ["id", "user.email", "email", "mailbox.email"]
column_filters = ["id", "user.email", "email", "mailbox.email"]
column_formatters = {
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
}
@action(
"disable_email_spoofing_check",
"Disable email spoofing protection",
"Disable email spoofing protection?",
)
def disable_email_spoofing_check_for(self, ids):
for alias in Alias.filter(Alias.id.in_(ids)):
if alias.disable_email_spoofing_check:
flash(
f"Email spoofing protection is already disabled on {alias.email}",
"warning",
)
else:
alias.disable_email_spoofing_check = True
flash(
f"Email spoofing protection is disabled on {alias.email}", "success"
)
Session.commit()
class MailboxAdmin(SLModelView):
form_base_class = SecureForm
column_searchable_list = ["id", "user.email", "email"]
column_filters = ["id", "user.email", "email"]
column_formatters = {
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
}
# class LifetimeCouponAdmin(SLModelView):
# can_edit = True
# can_create = True
class CouponAdmin(SLModelView):
form_base_class = SecureForm
can_edit = False
can_create = True
column_formatters = {
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
}
class ManualSubscriptionAdmin(SLModelView):
form_base_class = SecureForm
can_edit = True
column_searchable_list = ["id", "user.email"]
column_formatters = {
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
}
@action(
"extend_1y",
"Extend for 1 year",
"Extend 1 year more?",
)
def extend_1y(self, ids):
for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)):
ms.end_at = ms.end_at.shift(years=1)
flash(f"Extend subscription for 1 year for {ms.user}", "success")
AdminAuditLog.extend_subscription(
current_user.id, ms.user.id, ms.end_at, "1 year"
)
Session.commit()
@action(
"extend_1m",
"Extend for 1 month",
"Extend 1 month more?",
)
def extend_1m(self, ids):
for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)):
ms.end_at = ms.end_at.shift(months=1)
flash(f"Extend subscription for 1 month for {ms.user}", "success")
AdminAuditLog.extend_subscription(
current_user.id, ms.user.id, ms.end_at, "1 month"
)
Session.commit()
# class ClientAdmin(SLModelView):
# column_searchable_list = ["name", "description", "user.email"]
# column_exclude_list = ["oauth_client_secret", "home_url"]
# can_edit = True
class CustomDomainAdmin(SLModelView):
form_base_class = SecureForm
column_searchable_list = ["domain", "user.email", "user.id"]
column_exclude_list = ["ownership_txt_token"]
can_edit = False
column_formatters = {
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
}
class ReferralAdmin(SLModelView):
form_base_class = SecureForm
column_searchable_list = ["id", "user.email", "code", "name"]
column_filters = ["id", "user.email", "code", "name"]
column_formatters = {
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
}
def scaffold_list_columns(self):
ret = super().scaffold_list_columns()
ret.insert(0, "nb_user")
ret.insert(0, "nb_paid_user")
return ret
# class PayoutAdmin(SLModelView):
# column_searchable_list = ["id", "user.email"]
# column_filters = ["id", "user.email"]
# can_edit = True
# can_create = True
# can_delete = True
class AdminAuditLogAdmin(SLModelView):
form_base_class = SecureForm
column_searchable_list = ["admin.id", "admin.email", "model_id", "created_at"]
column_filters = ["admin.id", "admin.email", "model_id", "created_at"]
column_exclude_list = ["id"]
column_hide_backrefs = False
can_edit = False
can_create = False
can_delete = False
column_formatters = {
"action": _admin_action_formatter,
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
}
def _transactionalcomplaint_state_formatter(view, context, model, name):
return "{} ({})".format(ProviderComplaintState(model.state).name, model.state)
def _transactionalcomplaint_phase_formatter(view, context, model, name):
return Phase(model.phase).name
def _transactionalcomplaint_refused_email_id_formatter(view, context, model, name):
markupstring = "<a href='{}'>{}</a>".format(
url_for(".download_eml", id=model.id), model.refused_email.full_report_path
)
return Markup(markupstring)
class ProviderComplaintAdmin(SLModelView):
form_base_class = SecureForm
column_searchable_list = ["id", "user.id", "created_at"]
column_filters = ["user.id", "state"]
column_hide_backrefs = False
can_edit = False
can_create = False
can_delete = False
column_formatters = {
"created_at": _admin_date_formatter,
"updated_at": _admin_date_formatter,
"state": _transactionalcomplaint_state_formatter,
"phase": _transactionalcomplaint_phase_formatter,
"refused_email": _transactionalcomplaint_refused_email_id_formatter,
}
column_extra_row_actions = [ # Add a new action button
EndpointLinkRowAction("fa fa-check-square", ".mark_ok"),
]
def _get_complaint(self) -> Optional[ProviderComplaint]:
complain_id = request.args.get("id")
if complain_id is None:
flash("Missing id", "error")
return None
complaint = ProviderComplaint.get_by(id=complain_id)
if not complaint:
flash("Could not find complaint", "error")
return None
return complaint
@expose("/mark_ok", methods=["GET"])
def mark_ok(self):
complaint = self._get_complaint()
if not complaint:
return redirect("/admin/transactionalcomplaint/")
complaint.state = ProviderComplaintState.reviewed.value
Session.commit()
return redirect("/admin/transactionalcomplaint/")
@expose("/download_eml", methods=["GET"])
def download_eml(self):
complaint = self._get_complaint()
if not complaint:
return redirect("/admin/transactionalcomplaint/")
eml_path = complaint.refused_email.full_report_path
eml_data = s3.download_email(eml_path)
AdminAuditLog.downloaded_provider_complaint(current_user.id, complaint.id)
Session.commit()
return Response(
eml_data,
mimetype="message/rfc822",
headers={
"Content-Disposition": "attachment;filename={}".format(
complaint.refused_email.path
)
},
)
def _newsletter_plain_text_formatter(view, context, model: Newsletter, name):
# to display newsletter plain_text with linebreaks in the list view
return Markup(model.plain_text.replace("\n", "<br>"))
def _newsletter_html_formatter(view, context, model: Newsletter, name):
# to display newsletter html with linebreaks in the list view
return Markup(model.html.replace("\n", "<br>"))
class NewsletterAdmin(SLModelView):
form_base_class = SecureForm
list_template = "admin/model/newsletter-list.html"
edit_template = "admin/model/newsletter-edit.html"
edit_modal = False
can_edit = True
can_create = True
column_formatters = {
"plain_text": _newsletter_plain_text_formatter,
"html": _newsletter_html_formatter,
}
@action(
"send_newsletter_to_user",
"Send this newsletter to myself or the specified userID",
)
def send_newsletter_to_user(self, newsletter_ids):
user_id = request.form["user_id"]
if user_id:
user = User.get(user_id)
if not user:
flash(f"No such user with ID {user_id}", "error")
return
else:
flash("use the current user", "info")
user = current_user
for newsletter_id in newsletter_ids:
newsletter = Newsletter.get(newsletter_id)
sent, error_msg = send_newsletter_to_user(newsletter, user)
if sent:
flash(f"{newsletter} sent to {user}", "success")
else:
flash(error_msg, "error")
@action(
"send_newsletter_to_address",
"Send this newsletter to a specific address",
)
def send_newsletter_to_address(self, newsletter_ids):
to_address = request.form["to_address"]
if not to_address:
flash("to_address missing", "error")
return
for newsletter_id in newsletter_ids:
newsletter = Newsletter.get(newsletter_id)
# use the current_user for rendering email
sent, error_msg = send_newsletter_to_address(
newsletter, current_user, to_address
)
if sent:
flash(
f"{newsletter} sent to {to_address} with {current_user} context",
"success",
)
else:
flash(error_msg, "error")
@action(
"clone_newsletter",
"Clone this newsletter",
)
def clone_newsletter(self, newsletter_ids):
if len(newsletter_ids) != 1:
flash("you can only select 1 newsletter", "error")
return
newsletter_id = newsletter_ids[0]
newsletter: Newsletter = Newsletter.get(newsletter_id)
new_newsletter = Newsletter.create(
subject=newsletter.subject,
html=newsletter.html,
plain_text=newsletter.plain_text,
commit=True,
)
flash(f"Newsletter {new_newsletter.subject} has been cloned", "success")
class NewsletterUserAdmin(SLModelView):
form_base_class = SecureForm
column_searchable_list = ["id"]
column_filters = ["id", "user.email", "newsletter.subject"]
column_exclude_list = ["created_at", "updated_at", "id"]
can_edit = False
can_create = False
class DailyMetricAdmin(SLModelView):
form_base_class = SecureForm
column_exclude_list = ["created_at", "updated_at", "id"]
can_export = True
class MetricAdmin(SLModelView):
form_base_class = SecureForm
column_exclude_list = ["created_at", "updated_at", "id"]
can_export = True
class InvalidMailboxDomainAdmin(SLModelView):
form_base_class = SecureForm
can_create = True
can_delete = True
class EmailSearchResult:
no_match: bool = True
alias: Optional[Alias] = None
mailbox: list[Mailbox] = []
mailbox_count: int = 0
deleted_alias: Optional[DeletedAlias] = None
deleted_custom_alias: Optional[DomainDeletedAlias] = None
user: Optional[User] = None
@staticmethod
def from_email(email: str) -> EmailSearchResult:
output = EmailSearchResult()
alias = Alias.get_by(email=email)
if alias:
output.alias = alias
output.no_match = False
user = User.get_by(email=email)
if user:
output.user = user
output.no_match = False
mailboxes = (
Mailbox.filter_by(email=email).order_by(Mailbox.id.desc()).limit(10).all()
)
if mailboxes:
output.mailbox = mailboxes
output.mailbox_count = Mailbox.filter_by(email=email).count()
output.no_match = False
deleted_alias = DeletedAlias.get_by(email=email)
if deleted_alias:
output.deleted_alias = deleted_alias
output.no_match = False
domain_deleted_alias = DomainDeletedAlias.get_by(email=email)
if domain_deleted_alias:
output.domain_deleted_alias = domain_deleted_alias
output.no_match = False
return output
class EmailSearchHelpers:
@staticmethod
def mailbox_list(user: User) -> list[Mailbox]:
return (
Mailbox.filter_by(user_id=user.id)
.order_by(Mailbox.id.asc())
.limit(10)
.all()
)
@staticmethod
def mailbox_count(user: User) -> int:
return Mailbox.filter_by(user_id=user.id).order_by(Mailbox.id.desc()).count()
@staticmethod
def alias_list(user: User) -> list[Alias]:
return (
Alias.filter_by(user_id=user.id).order_by(Alias.id.desc()).limit(10).all()
)
@staticmethod
def alias_count(user: User) -> int:
return Alias.filter_by(user_id=user.id).count()
@staticmethod
def partner_user(user: User) -> Optional[PartnerUser]:
return PartnerUser.get_by(user_id=user.id)
class EmailSearchAdmin(BaseView):
def is_accessible(self):
return current_user.is_authenticated and current_user.is_admin
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
flash("You don't have access to the admin page", "error")
return redirect(url_for("dashboard.index", next=request.url))
@expose("/", methods=["GET", "POST"])
def index(self):
search = EmailSearchResult()
email = request.args.get("email")
if email is not None and len(email) > 0:
email = email.strip()
search = EmailSearchResult.from_email(email)
return self.render(
"admin/email_search.html",
email=email,
data=search,
helper=EmailSearchHelpers,
)

192
app/alias_suffix.py Normal file
View File

@ -0,0 +1,192 @@
from __future__ import annotations
import json
from dataclasses import asdict, dataclass
from typing import Optional
import itsdangerous
from app import config
from app.log import LOG
from app.models import User, AliasOptions, SLDomain
signer = itsdangerous.TimestampSigner(config.CUSTOM_ALIAS_SECRET)
@dataclass
class AliasSuffix:
# whether this is a custom domain
is_custom: bool
# Suffix
suffix: str
# Suffix signature
signed_suffix: str
# whether this is a premium SL domain. Not apply to custom domain
is_premium: bool
# can be either Custom or SL domain
domain: str
# if custom domain, whether the custom domain has MX verified, i.e. can receive emails
mx_verified: bool = True
def serialize(self):
return json.dumps(asdict(self))
@classmethod
def deserialize(cls, data: str) -> AliasSuffix:
return AliasSuffix(**json.loads(data))
def check_suffix_signature(signed_suffix: str) -> Optional[str]:
# hypothesis: user will click on the button in the 600 secs
try:
return signer.unsign(signed_suffix, max_age=600).decode()
except itsdangerous.BadSignature:
return None
def verify_prefix_suffix(
user: User, alias_prefix, alias_suffix, alias_options: Optional[AliasOptions] = None
) -> bool:
"""verify if user could create an alias with the given prefix and suffix"""
if not alias_prefix or not alias_suffix: # should be caught on frontend
return False
user_custom_domains = [cd.domain for cd in user.verified_custom_domains()]
# make sure alias_suffix is either .random_word@simplelogin.co or @my-domain.com
alias_suffix = alias_suffix.strip()
# alias_domain_prefix is either a .random_word or ""
alias_domain_prefix, alias_domain = alias_suffix.split("@", 1)
# alias_domain must be either one of user custom domains or built-in domains
if alias_domain not in user.available_alias_domains(alias_options=alias_options):
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
return False
# SimpleLogin domain case:
# 1) alias_suffix must start with "." and
# 2) alias_domain_prefix must come from the word list
available_sl_domains = [
sl_domain.domain
for sl_domain in user.get_sl_domains(alias_options=alias_options)
]
if (
alias_domain in available_sl_domains
and alias_domain not in user_custom_domains
# when DISABLE_ALIAS_SUFFIX is true, alias_domain_prefix is empty
and not config.DISABLE_ALIAS_SUFFIX
):
if not alias_domain_prefix.startswith("."):
LOG.e("User %s submits a wrong alias suffix %s", user, alias_suffix)
return False
else:
if alias_domain not in user_custom_domains:
if not config.DISABLE_ALIAS_SUFFIX:
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
return False
if alias_domain not in available_sl_domains:
LOG.e("wrong alias suffix %s, user %s", alias_suffix, user)
return False
return True
def get_alias_suffixes(
user: User, alias_options: Optional[AliasOptions] = None
) -> [AliasSuffix]:
"""
Similar to as get_available_suffixes() but also return custom domain that doesn't have MX set up.
"""
user_custom_domains = user.verified_custom_domains()
alias_suffixes: [AliasSuffix] = []
# put custom domain first
# for each user domain, generate both the domain and a random suffix version
for custom_domain in user_custom_domains:
if custom_domain.random_prefix_generation:
suffix = (
f".{user.get_random_alias_suffix(custom_domain)}@{custom_domain.domain}"
)
alias_suffix = AliasSuffix(
is_custom=True,
suffix=suffix,
signed_suffix=signer.sign(suffix).decode(),
is_premium=False,
domain=custom_domain.domain,
mx_verified=custom_domain.verified,
)
if user.default_alias_custom_domain_id == custom_domain.id:
alias_suffixes.insert(0, alias_suffix)
else:
alias_suffixes.append(alias_suffix)
suffix = f"@{custom_domain.domain}"
alias_suffix = AliasSuffix(
is_custom=True,
suffix=suffix,
signed_suffix=signer.sign(suffix).decode(),
is_premium=False,
domain=custom_domain.domain,
mx_verified=custom_domain.verified,
)
# put the default domain to top
# only if random_prefix_generation isn't enabled
if (
user.default_alias_custom_domain_id == custom_domain.id
and not custom_domain.random_prefix_generation
):
alias_suffixes.insert(0, alias_suffix)
else:
alias_suffixes.append(alias_suffix)
# then SimpleLogin domain
sl_domains = user.get_sl_domains(alias_options=alias_options)
default_domain_found = False
for sl_domain in sl_domains:
prefix = (
"" if config.DISABLE_ALIAS_SUFFIX else f".{user.get_random_alias_suffix()}"
)
suffix = f"{prefix}@{sl_domain.domain}"
alias_suffix = AliasSuffix(
is_custom=False,
suffix=suffix,
signed_suffix=signer.sign(suffix).decode(),
is_premium=sl_domain.premium_only,
domain=sl_domain.domain,
mx_verified=True,
)
# No default or this is not the default
if (
user.default_alias_public_domain_id is None
or user.default_alias_public_domain_id != sl_domain.id
):
alias_suffixes.append(alias_suffix)
else:
default_domain_found = True
alias_suffixes.insert(0, alias_suffix)
if not default_domain_found:
domain_conditions = {"id": user.default_alias_public_domain_id, "hidden": False}
if not user.is_premium():
domain_conditions["premium_only"] = False
sl_domain = SLDomain.get_by(**domain_conditions)
if sl_domain:
prefix = (
""
if config.DISABLE_ALIAS_SUFFIX
else f".{user.get_random_alias_suffix()}"
)
suffix = f"{prefix}@{sl_domain.domain}"
alias_suffix = AliasSuffix(
is_custom=False,
suffix=suffix,
signed_suffix=signer.sign(suffix).decode(),
is_premium=sl_domain.premium_only,
domain=sl_domain.domain,
mx_verified=True,
)
alias_suffixes.insert(0, alias_suffix)
return alias_suffixes

520
app/alias_utils.py Normal file
View File

@ -0,0 +1,520 @@
import csv
from io import StringIO
import re
from typing import Optional, Tuple
from email_validator import validate_email, EmailNotValidError
from sqlalchemy.exc import IntegrityError, DataError
from flask import make_response
from app.config import (
BOUNCE_PREFIX_FOR_REPLY_PHASE,
BOUNCE_PREFIX,
BOUNCE_SUFFIX,
VERP_PREFIX,
)
from app.db import Session
from app.email_utils import (
get_email_domain_part,
send_cannot_create_directory_alias,
can_create_directory_for_address,
send_cannot_create_directory_alias_disabled,
get_email_local_part,
send_cannot_create_domain_alias,
send_email,
render,
)
from app.errors import AliasInTrashError
from app.events.event_dispatcher import EventDispatcher
from app.events.generated.event_pb2 import (
AliasDeleted,
AliasStatusChanged,
EventContent,
)
from app.log import LOG
from app.models import (
Alias,
AliasDeleteReason,
CustomDomain,
Directory,
User,
DeletedAlias,
DomainDeletedAlias,
AliasMailbox,
Mailbox,
EmailLog,
Contact,
AutoCreateRule,
AliasUsedOn,
ClientUser,
)
from app.regex_utils import regex_match
def get_user_if_alias_would_auto_create(
address: str, notify_user: bool = False
) -> Optional[User]:
banned_prefix = f"{VERP_PREFIX}."
if address.startswith(banned_prefix):
LOG.w("alias %s can't start with %s", address, banned_prefix)
return None
try:
# Prevent addresses with unicode characters (🤯) in them for now.
validate_email(address, check_deliverability=False, allow_smtputf8=False)
except EmailNotValidError:
LOG.i(f"Not creating alias for {address} because email is invalid")
return None
domain_and_rule = check_if_alias_can_be_auto_created_for_custom_domain(
address, notify_user=notify_user
)
if DomainDeletedAlias.get_by(email=address):
LOG.i(
f"Not creating alias for {address} because it was previously deleted for this domain"
)
return None
if domain_and_rule:
return domain_and_rule[0].user
directory = check_if_alias_can_be_auto_created_for_a_directory(
address, notify_user=notify_user
)
if directory:
return directory.user
return None
def check_if_alias_can_be_auto_created_for_custom_domain(
address: str, notify_user: bool = True
) -> Optional[Tuple[CustomDomain, Optional[AutoCreateRule]]]:
"""
Check if this address would generate an auto created alias.
If that's the case return the domain that would create it and the rule that triggered it.
If there's no rule it's a catchall creation
"""
alias_domain = get_email_domain_part(address)
custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain)
if not custom_domain:
LOG.i(
f"Cannot auto-create custom domain alias for {address} because there's no custom domain for {alias_domain}"
)
return None
user: User = custom_domain.user
if user.disabled:
LOG.i("Disabled user %s can't create new alias via custom domain", user)
return None
if not user.can_create_new_alias():
LOG.d(f"{user} can't create new custom-domain alias {address}")
if notify_user:
send_cannot_create_domain_alias(custom_domain.user, address, alias_domain)
return None
if not custom_domain.catch_all:
if len(custom_domain.auto_create_rules) == 0:
LOG.i(
f"Cannot create alias {address} for domain {custom_domain} because it has no catch-all and no rules"
)
return None
local = get_email_local_part(address)
for rule in custom_domain.auto_create_rules:
if regex_match(rule.regex, local):
LOG.d(
"%s passes %s on %s",
address,
rule.regex,
custom_domain,
)
return custom_domain, rule
else: # no rule passes
LOG.d(f"No rule matches auto-create {address} for domain {custom_domain}")
return None
LOG.d("Create alias via catchall")
return custom_domain, None
def check_if_alias_can_be_auto_created_for_a_directory(
address: str, notify_user: bool = True
) -> Optional[Directory]:
"""
Try to create an alias with directory
If an alias would be created, return the dictionary that would trigger the creation. Otherwise, return None.
"""
# check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
if not can_create_directory_for_address(address):
return None
# alias contains one of the 3 special directory separator: "/", "+" or "#"
if "/" in address:
sep = "/"
elif "+" in address:
sep = "+"
elif "#" in address:
sep = "#"
else:
# if there's no directory separator in the alias, no way to auto-create it
LOG.info(f"Cannot auto-create {address} since it has no directory separator")
return None
directory_name = address[: address.find(sep)]
LOG.d("directory_name %s", directory_name)
directory = Directory.get_by(name=directory_name)
if not directory:
LOG.info(
f"Cannot auto-create {address} because there is no directory for {directory_name}"
)
return None
user: User = directory.user
if user.disabled:
LOG.i("Disabled %s can't create new alias with directory", user)
return None
if not user.can_create_new_alias():
LOG.d(
f"{user} can't create new directory alias {address} because user cannot create aliases"
)
if notify_user:
send_cannot_create_directory_alias(user, address, directory_name)
return None
if directory.disabled:
LOG.d(
f"{user} can't create new directory alias {address} bcause directory is disabled"
)
if notify_user:
send_cannot_create_directory_alias_disabled(user, address, directory_name)
return None
return directory
def try_auto_create(address: str) -> Optional[Alias]:
"""Try to auto-create the alias using directory or catch-all domain"""
# VERP for reply phase is {BOUNCE_PREFIX_FOR_REPLY_PHASE}+{email_log.id}+@{alias_domain}
if address.startswith(f"{BOUNCE_PREFIX_FOR_REPLY_PHASE}+") and "+@" in address:
LOG.e("alias %s can't start with %s", address, BOUNCE_PREFIX_FOR_REPLY_PHASE)
return None
# VERP for forward phase is BOUNCE_PREFIX + email_log.id + BOUNCE_SUFFIX
if address.startswith(BOUNCE_PREFIX) and address.endswith(BOUNCE_SUFFIX):
LOG.e("alias %s can't start with %s", address, BOUNCE_PREFIX)
return None
try:
# NOT allow unicode for now
validate_email(address, check_deliverability=False, allow_smtputf8=False)
except EmailNotValidError:
return None
alias = try_auto_create_via_domain(address)
if not alias:
alias = try_auto_create_directory(address)
return alias
def try_auto_create_directory(address: str) -> Optional[Alias]:
"""
Try to create an alias with directory
"""
directory = check_if_alias_can_be_auto_created_for_a_directory(
address, notify_user=True
)
if not directory:
return None
try:
LOG.d("create alias %s for directory %s", address, directory)
mailboxes = directory.mailboxes
alias = Alias.create(
email=address,
user_id=directory.user_id,
directory_id=directory.id,
mailbox_id=mailboxes[0].id,
)
if not directory.user.disable_automatic_alias_note:
alias.note = f"Created by directory {directory.name}"
Session.flush()
for i in range(1, len(mailboxes)):
AliasMailbox.create(
alias_id=alias.id,
mailbox_id=mailboxes[i].id,
)
Session.commit()
return alias
except AliasInTrashError:
LOG.w(
"Alias %s was deleted before, cannot auto-create using directory %s, user %s",
address,
directory.name,
directory.user,
)
return None
except IntegrityError:
LOG.w("Alias %s already exists", address)
Session.rollback()
alias = Alias.get_by(email=address)
return alias
def try_auto_create_via_domain(address: str) -> Optional[Alias]:
"""Try to create an alias with catch-all or auto-create rules on custom domain"""
can_create = check_if_alias_can_be_auto_created_for_custom_domain(address)
if not can_create:
return None
custom_domain, rule = can_create
if rule:
alias_note = f"Created by rule {rule.order} with regex {rule.regex}"
mailboxes = rule.mailboxes
else:
alias_note = "Created by catchall option"
mailboxes = custom_domain.mailboxes
# a rule can have 0 mailboxes. Happened when a mailbox is deleted
if not mailboxes:
LOG.d(
"use %s default mailbox for %s %s",
custom_domain.user,
address,
custom_domain,
)
mailboxes = [custom_domain.user.default_mailbox]
try:
LOG.d("create alias %s for domain %s", address, custom_domain)
alias = Alias.create(
email=address,
user_id=custom_domain.user_id,
custom_domain_id=custom_domain.id,
automatic_creation=True,
mailbox_id=mailboxes[0].id,
)
if not custom_domain.user.disable_automatic_alias_note:
alias.note = alias_note
Session.flush()
for i in range(1, len(mailboxes)):
AliasMailbox.create(
alias_id=alias.id,
mailbox_id=mailboxes[i].id,
)
Session.commit()
return alias
except AliasInTrashError:
LOG.w(
"Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s",
address,
custom_domain,
custom_domain.user,
)
return None
except IntegrityError:
LOG.w("Alias %s already exists", address)
Session.rollback()
alias = Alias.get_by(email=address)
return alias
except DataError:
LOG.w("Cannot create alias %s", address)
Session.rollback()
return None
def delete_alias(
alias: Alias,
user: User,
reason: AliasDeleteReason = AliasDeleteReason.Unspecified,
commit: bool = False,
):
"""
Delete an alias and add it to either global or domain trash
Should be used instead of Alias.delete, DomainDeletedAlias.create, DeletedAlias.create
"""
LOG.i(f"User {user} has deleted alias {alias}")
# save deleted alias to either global or domain tra
if alias.custom_domain_id:
if not DomainDeletedAlias.get_by(
email=alias.email, domain_id=alias.custom_domain_id
):
domain_deleted_alias = DomainDeletedAlias(
user_id=user.id,
email=alias.email,
domain_id=alias.custom_domain_id,
reason=reason,
)
Session.add(domain_deleted_alias)
Session.commit()
LOG.i(
f"Moving {alias} to domain {alias.custom_domain_id} trash {domain_deleted_alias}"
)
else:
if not DeletedAlias.get_by(email=alias.email):
deleted_alias = DeletedAlias(email=alias.email, reason=reason)
Session.add(deleted_alias)
Session.commit()
LOG.i(f"Moving {alias} to global trash {deleted_alias}")
alias_id = alias.id
alias_email = alias.email
Alias.filter(Alias.id == alias.id).delete()
Session.commit()
EventDispatcher.send_event(
user,
EventContent(alias_deleted=AliasDeleted(id=alias_id, email=alias_email)),
)
if commit:
Session.commit()
def aliases_for_mailbox(mailbox: Mailbox) -> [Alias]:
"""
get list of aliases for a given mailbox
"""
ret = set(Alias.filter(Alias.mailbox_id == mailbox.id).all())
for alias in (
Session.query(Alias)
.join(AliasMailbox, Alias.id == AliasMailbox.alias_id)
.filter(AliasMailbox.mailbox_id == mailbox.id)
):
ret.add(alias)
return list(ret)
def nb_email_log_for_mailbox(mailbox: Mailbox):
aliases = aliases_for_mailbox(mailbox)
alias_ids = [alias.id for alias in aliases]
return (
Session.query(EmailLog)
.join(Contact, EmailLog.contact_id == Contact.id)
.filter(Contact.alias_id.in_(alias_ids))
.count()
)
# Only lowercase letters, numbers, dots (.), dashes (-) and underscores (_) are currently supported
_ALIAS_PREFIX_PATTERN = r"[0-9a-z-_.]{1,}"
def check_alias_prefix(alias_prefix) -> bool:
if len(alias_prefix) > 40:
return False
if re.fullmatch(_ALIAS_PREFIX_PATTERN, alias_prefix) is None:
return False
return True
def alias_export_csv(user, csv_direct_export=False):
"""
Get user aliases as importable CSV file
Output:
Importable CSV file
"""
data = [["alias", "note", "enabled", "mailboxes"]]
for alias in Alias.filter_by(user_id=user.id).all(): # type: Alias
# Always put the main mailbox first
# It is seen a primary while importing
alias_mailboxes = alias.mailboxes
alias_mailboxes.insert(
0, alias_mailboxes.pop(alias_mailboxes.index(alias.mailbox))
)
mailboxes = " ".join([mailbox.email for mailbox in alias_mailboxes])
data.append([alias.email, alias.note, alias.enabled, mailboxes])
si = StringIO()
cw = csv.writer(si)
cw.writerows(data)
if csv_direct_export:
return si.getvalue()
output = make_response(si.getvalue())
output.headers["Content-Disposition"] = "attachment; filename=aliases.csv"
output.headers["Content-type"] = "text/csv"
return output
def transfer_alias(alias, new_user, new_mailboxes: [Mailbox]):
# cannot transfer alias which is used for receiving newsletter
if User.get_by(newsletter_alias_id=alias.id):
raise Exception("Cannot transfer alias that's used to receive newsletter")
# update user_id
Session.query(Contact).filter(Contact.alias_id == alias.id).update(
{"user_id": new_user.id}
)
Session.query(AliasUsedOn).filter(AliasUsedOn.alias_id == alias.id).update(
{"user_id": new_user.id}
)
Session.query(ClientUser).filter(ClientUser.alias_id == alias.id).update(
{"user_id": new_user.id}
)
# remove existing mailboxes from the alias
Session.query(AliasMailbox).filter(AliasMailbox.alias_id == alias.id).delete()
# set mailboxes
alias.mailbox_id = new_mailboxes.pop().id
for mb in new_mailboxes:
AliasMailbox.create(alias_id=alias.id, mailbox_id=mb.id)
# alias has never been transferred before
if not alias.original_owner_id:
alias.original_owner_id = alias.user_id
# inform previous owner
old_user = alias.user
send_email(
old_user.email,
f"Alias {alias.email} has been received",
render(
"transactional/alias-transferred.txt",
user=old_user,
alias=alias,
),
render(
"transactional/alias-transferred.html",
user=old_user,
alias=alias,
),
)
# now the alias belongs to the new user
alias.user_id = new_user.id
# set some fields back to default
alias.disable_pgp = False
alias.pinned = False
Session.commit()
def change_alias_status(alias: Alias, enabled: bool, commit: bool = False):
LOG.i(f"Changing alias {alias} enabled to {enabled}")
alias.enabled = enabled
event = AliasStatusChanged(
id=alias.id,
email=alias.email,
enabled=enabled,
created_at=int(alias.created_at.timestamp),
)
EventDispatcher.send_event(alias.user, EventContent(alias_status_change=event))
if commit:
Session.commit()

View File

@ -1,9 +1,37 @@
from .views import ( from .views import (
alias_options, alias_options,
new_custom_alias, new_custom_alias,
custom_domain,
new_random_alias, new_random_alias,
user_info, user_info,
auth_login, auth,
auth_mfa, auth_mfa,
alias, alias,
apple,
mailbox,
notification,
setting,
export,
phone,
sudo,
user,
) )
__all__ = [
"alias_options",
"new_custom_alias",
"custom_domain",
"new_random_alias",
"user_info",
"auth",
"auth_mfa",
"alias",
"apple",
"mailbox",
"notification",
"setting",
"export",
"phone",
"sudo",
"user",
]

View File

@ -1,30 +1,73 @@
from functools import wraps from functools import wraps
from typing import Tuple, Optional
import arrow import arrow
from flask import Blueprint, request, jsonify, g from flask import Blueprint, request, jsonify, g
from flask_login import current_user
from app.extensions import db from app.db import Session
from app.models import ApiKey from app.models import ApiKey
api_bp = Blueprint(name="api", import_name=__name__, url_prefix="/api") api_bp = Blueprint(name="api", import_name=__name__, url_prefix="/api")
SUDO_MODE_MINUTES_VALID = 5
def verify_api_key(f):
@wraps(f)
def decorated(*args, **kwargs):
api_code = request.headers.get("Authentication")
api_key = ApiKey.get_by(code=api_code)
if not api_key: def authorize_request() -> Optional[Tuple[str, int]]:
api_code = request.headers.get("Authentication")
api_key = ApiKey.get_by(code=api_code)
if not api_key:
if current_user.is_authenticated:
# if current_user.is_authenticated and request.headers.get(
# constants.HEADER_ALLOW_API_COOKIES
# ):
g.user = current_user
else:
return jsonify(error="Wrong api key"), 401 return jsonify(error="Wrong api key"), 401
else:
# Update api key stats # Update api key stats
api_key.last_used = arrow.now() api_key.last_used = arrow.now()
api_key.times += 1 api_key.times += 1
db.session.commit() Session.commit()
g.user = api_key.user g.user = api_key.user
if g.user.disabled:
return jsonify(error="Disabled account"), 403
if not g.user.is_active():
return jsonify(error="Account does not exist"), 401
g.api_key = api_key
return None
def check_sudo_mode_is_active(api_key: ApiKey) -> bool:
return api_key.sudo_mode_at and g.api_key.sudo_mode_at >= arrow.now().shift(
minutes=-SUDO_MODE_MINUTES_VALID
)
def require_api_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
error_return = authorize_request()
if error_return:
return error_return
return f(*args, **kwargs)
return decorated
def require_api_sudo(f):
@wraps(f)
def decorated(*args, **kwargs):
error_return = authorize_request()
if error_return:
return error_return
if not check_sudo_mode_is_active(g.api_key):
return jsonify(error="Need sudo"), 440
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated return decorated

382
app/api/serializer.py Normal file
View File

@ -0,0 +1,382 @@
from dataclasses import dataclass
from typing import Optional
from arrow import Arrow
from sqlalchemy import or_, func, case, and_
from sqlalchemy.orm import joinedload
from app.config import PAGE_LIMIT
from app.db import Session
from app.models import (
Alias,
Contact,
EmailLog,
Mailbox,
AliasMailbox,
CustomDomain,
User,
)
@dataclass
class AliasInfo:
alias: Alias
mailbox: Mailbox
mailboxes: [Mailbox]
nb_forward: int
nb_blocked: int
nb_reply: int
latest_email_log: EmailLog = None
latest_contact: Contact = None
custom_domain: Optional[CustomDomain] = None
def contain_mailbox(self, mailbox_id: int) -> bool:
return mailbox_id in [m.id for m in self.mailboxes]
def serialize_alias_info(alias_info: AliasInfo) -> dict:
return {
# Alias field
"id": alias_info.alias.id,
"email": alias_info.alias.email,
"creation_date": alias_info.alias.created_at.format(),
"creation_timestamp": alias_info.alias.created_at.timestamp,
"enabled": alias_info.alias.enabled,
"note": alias_info.alias.note,
# activity
"nb_forward": alias_info.nb_forward,
"nb_block": alias_info.nb_blocked,
"nb_reply": alias_info.nb_reply,
}
def serialize_alias_info_v2(alias_info: AliasInfo) -> dict:
res = {
# Alias field
"id": alias_info.alias.id,
"email": alias_info.alias.email,
"creation_date": alias_info.alias.created_at.format(),
"creation_timestamp": alias_info.alias.created_at.timestamp,
"enabled": alias_info.alias.enabled,
"note": alias_info.alias.note,
"name": alias_info.alias.name,
# activity
"nb_forward": alias_info.nb_forward,
"nb_block": alias_info.nb_blocked,
"nb_reply": alias_info.nb_reply,
# mailbox
"mailbox": {"id": alias_info.mailbox.id, "email": alias_info.mailbox.email},
"mailboxes": [
{"id": mailbox.id, "email": mailbox.email}
for mailbox in alias_info.mailboxes
],
"support_pgp": alias_info.alias.mailbox_support_pgp(),
"disable_pgp": alias_info.alias.disable_pgp,
"latest_activity": None,
"pinned": alias_info.alias.pinned,
}
if alias_info.latest_email_log:
email_log = alias_info.latest_email_log
contact = alias_info.latest_contact
# latest activity
res["latest_activity"] = {
"timestamp": email_log.created_at.timestamp,
"action": email_log.get_action(),
"contact": {
"email": contact.website_email,
"name": contact.name,
"reverse_alias": contact.website_send_to(),
},
}
return res
def serialize_contact(contact: Contact, existed=False) -> dict:
res = {
"id": contact.id,
"creation_date": contact.created_at.format(),
"creation_timestamp": contact.created_at.timestamp,
"last_email_sent_date": None,
"last_email_sent_timestamp": None,
"contact": contact.website_email,
"reverse_alias": contact.website_send_to(),
"reverse_alias_address": contact.reply_email,
"existed": existed,
"block_forward": contact.block_forward,
}
email_log: EmailLog = contact.last_reply()
if email_log:
res["last_email_sent_date"] = email_log.created_at.format()
res["last_email_sent_timestamp"] = email_log.created_at.timestamp
return res
def get_alias_infos_with_pagination(user, page_id=0, query=None) -> [AliasInfo]:
ret = []
q = (
Session.query(Alias)
.options(joinedload(Alias.mailbox))
.filter(Alias.user_id == user.id)
.order_by(Alias.created_at.desc())
)
if query:
q = q.filter(
or_(Alias.email.ilike(f"%{query}%"), Alias.note.ilike(f"%{query}%"))
)
q = q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT)
for alias in q:
ret.append(get_alias_info(alias))
return ret
def get_alias_infos_with_pagination_v3(
user,
page_id=0,
query=None,
sort=None,
alias_filter=None,
mailbox_id=None,
directory_id=None,
page_limit=PAGE_LIMIT,
page_size=PAGE_LIMIT,
) -> [AliasInfo]:
q = construct_alias_query(user)
if query:
q = q.filter(
or_(
Alias.email.ilike(f"%{query}%"),
Alias.note.ilike(f"%{query}%"),
# can't use match() here as it uses to_tsquery that expected a tsquery input
# Alias.ts_vector.match(query),
Alias.ts_vector.op("@@")(func.plainto_tsquery("english", query)),
Alias.name.ilike(f"%{query}%"),
)
)
if mailbox_id:
q = q.join(
AliasMailbox, Alias.id == AliasMailbox.alias_id, isouter=True
).filter(
or_(Alias.mailbox_id == mailbox_id, AliasMailbox.mailbox_id == mailbox_id)
)
if directory_id:
q = q.filter(Alias.directory_id == directory_id)
if alias_filter == "enabled":
q = q.filter(Alias.enabled)
elif alias_filter == "disabled":
q = q.filter(Alias.enabled.is_(False))
elif alias_filter == "pinned":
q = q.filter(Alias.pinned)
elif alias_filter == "hibp":
q = q.filter(Alias.hibp_breaches.any())
if sort == "old2new":
q = q.order_by(Alias.created_at)
elif sort == "new2old":
q = q.order_by(Alias.created_at.desc())
elif sort == "a2z":
q = q.order_by(Alias.email)
elif sort == "z2a":
q = q.order_by(Alias.email.desc())
else:
# default sorting
latest_activity = case(
[
(Alias.created_at > EmailLog.created_at, Alias.created_at),
(Alias.created_at < EmailLog.created_at, EmailLog.created_at),
],
else_=Alias.created_at,
)
q = q.order_by(Alias.pinned.desc())
q = q.order_by(latest_activity.desc())
q = q.limit(page_limit).offset(page_id * page_size)
ret = []
for alias, contact, email_log, nb_reply, nb_blocked, nb_forward in list(q):
ret.append(
AliasInfo(
alias=alias,
mailbox=alias.mailbox,
mailboxes=alias.mailboxes,
nb_forward=nb_forward,
nb_blocked=nb_blocked,
nb_reply=nb_reply,
latest_email_log=email_log,
latest_contact=contact,
custom_domain=alias.custom_domain,
)
)
return ret
def get_alias_info(alias: Alias) -> AliasInfo:
q = (
Session.query(Contact, EmailLog)
.filter(Contact.alias_id == alias.id)
.filter(EmailLog.contact_id == Contact.id)
)
alias_info = AliasInfo(
alias=alias,
nb_blocked=0,
nb_forward=0,
nb_reply=0,
mailbox=alias.mailbox,
mailboxes=[alias.mailbox],
)
for _, el in q:
if el.is_reply:
alias_info.nb_reply += 1
elif el.blocked:
alias_info.nb_blocked += 1
else:
alias_info.nb_forward += 1
return alias_info
def get_alias_info_v2(alias: Alias, mailbox=None) -> AliasInfo:
if not mailbox:
mailbox = alias.mailbox
q = (
Session.query(Contact, EmailLog)
.filter(Contact.alias_id == alias.id)
.filter(EmailLog.contact_id == Contact.id)
)
latest_activity: Arrow = alias.created_at
latest_email_log = None
latest_contact = None
alias_info = AliasInfo(
alias=alias,
nb_blocked=0,
nb_forward=0,
nb_reply=0,
mailbox=mailbox,
mailboxes=[mailbox],
)
for m in alias._mailboxes:
alias_info.mailboxes.append(m)
# remove duplicates
# can happen that alias.mailbox_id also appears in AliasMailbox table
alias_info.mailboxes = list(set(alias_info.mailboxes))
for contact, email_log in q:
if email_log.is_reply:
alias_info.nb_reply += 1
elif email_log.blocked:
alias_info.nb_blocked += 1
else:
alias_info.nb_forward += 1
if email_log.created_at > latest_activity:
latest_activity = email_log.created_at
latest_email_log = email_log
latest_contact = contact
alias_info.latest_contact = latest_contact
alias_info.latest_email_log = latest_email_log
return alias_info
def get_alias_contacts(alias, page_id: int) -> [dict]:
q = (
Contact.filter_by(alias_id=alias.id)
.order_by(Contact.id.desc())
.limit(PAGE_LIMIT)
.offset(page_id * PAGE_LIMIT)
)
res = []
for fe in q.all():
res.append(serialize_contact(fe))
return res
def get_alias_info_v3(user: User, alias_id: int) -> AliasInfo:
# use the same query construction in get_alias_infos_with_pagination_v3
q = construct_alias_query(user)
q = q.filter(Alias.id == alias_id)
for alias, contact, email_log, nb_reply, nb_blocked, nb_forward in q:
return AliasInfo(
alias=alias,
mailbox=alias.mailbox,
mailboxes=alias.mailboxes,
nb_forward=nb_forward,
nb_blocked=nb_blocked,
nb_reply=nb_reply,
latest_email_log=email_log,
latest_contact=contact,
custom_domain=alias.custom_domain,
)
def construct_alias_query(user: User):
# subquery on alias annotated with nb_reply, nb_blocked, nb_forward, max_created_at, latest_email_log_created_at
alias_activity_subquery = (
Session.query(
Alias.id,
func.sum(case([(EmailLog.is_reply, 1)], else_=0)).label("nb_reply"),
func.sum(
case(
[(and_(EmailLog.is_reply.is_(False), EmailLog.blocked), 1)],
else_=0,
)
).label("nb_blocked"),
func.sum(
case(
[
(
and_(
EmailLog.is_reply.is_(False),
EmailLog.blocked.is_(False),
),
1,
)
],
else_=0,
)
).label("nb_forward"),
)
.join(EmailLog, Alias.id == EmailLog.alias_id, isouter=True)
.filter(Alias.user_id == user.id)
.group_by(Alias.id)
.subquery()
)
return (
Session.query(
Alias,
Contact,
EmailLog,
alias_activity_subquery.c.nb_reply,
alias_activity_subquery.c.nb_blocked,
alias_activity_subquery.c.nb_forward,
)
.options(joinedload(Alias.hibp_breaches))
.options(joinedload(Alias.custom_domain))
.join(EmailLog, Alias.last_email_log_id == EmailLog.id, isouter=True)
.join(Contact, EmailLog.contact_id == Contact.id, isouter=True)
.filter(Alias.id == alias_activity_subquery.c.id)
)

View File

@ -1,17 +1,38 @@
from deprecated import deprecated
from flask import g from flask import g
from flask import jsonify, request from flask import jsonify
from flask_cors import cross_origin from flask import request
from app.api.base import api_bp, verify_api_key from app import alias_utils
from app.api.base import api_bp, require_api_auth
from app.api.serializer import (
AliasInfo,
serialize_alias_info,
serialize_contact,
get_alias_infos_with_pagination,
get_alias_contacts,
serialize_alias_info_v2,
get_alias_info_v2,
get_alias_infos_with_pagination_v3,
)
from app.dashboard.views.alias_contact_manager import create_contact
from app.dashboard.views.alias_log import get_alias_log from app.dashboard.views.alias_log import get_alias_log
from app.dashboard.views.index import get_alias_info, AliasInfo from app.db import Session
from app.extensions import db from app.errors import (
from app.models import GenEmail CannotCreateContactForReverseAlias,
ErrContactErrorUpgradeNeeded,
ErrContactAlreadyExists,
ErrAddressInvalid,
)
from app.extensions import limiter
from app.log import LOG
from app.models import Alias, Contact, Mailbox, AliasMailbox, AliasDeleteReason
@api_bp.route("/aliases") @deprecated
@cross_origin() @api_bp.route("/aliases", methods=["GET", "POST"])
@verify_api_key @require_api_auth
@limiter.limit("10/minute", key_func=lambda: g.user.id)
def get_aliases(): def get_aliases():
""" """
Get aliases Get aliases
@ -35,32 +56,96 @@ def get_aliases():
except (ValueError, TypeError): except (ValueError, TypeError):
return jsonify(error="page_id must be provided in request query"), 400 return jsonify(error="page_id must be provided in request query"), 400
aliases: [AliasInfo] = get_alias_info(user, page_id=page_id) query = None
data = request.get_json(silent=True)
if data:
query = data.get("query")
alias_infos: [AliasInfo] = get_alias_infos_with_pagination(
user, page_id=page_id, query=query
)
return ( return (
jsonify( jsonify(
aliases=[ aliases=[serialize_alias_info(alias_info) for alias_info in alias_infos]
{ ),
"id": alias.id, 200,
"email": alias.gen_email.email, )
"creation_date": alias.gen_email.created_at.format(),
"creation_timestamp": alias.gen_email.created_at.timestamp,
"nb_forward": alias.nb_forward, @api_bp.route("/v2/aliases", methods=["GET", "POST"])
"nb_block": alias.nb_blocked, @require_api_auth
"nb_reply": alias.nb_reply, @limiter.limit("50/minute", key_func=lambda: g.user.id)
"enabled": alias.gen_email.enabled, def get_aliases_v2():
"note": alias.note, """
} Get aliases
for alias in aliases Input:
] page_id: in query
pinned: in query
disabled: in query
enabled: in query
Output:
- aliases: list of alias:
- id
- email
- creation_date
- creation_timestamp
- nb_forward
- nb_block
- nb_reply
- note
- mailbox
- mailboxes
- support_pgp
- disable_pgp
- latest_activity: null if no activity.
- timestamp
- action: forward|reply|block|bounced
- contact:
- email
- name
- reverse_alias
"""
user = g.user
try:
page_id = int(request.args.get("page_id"))
except (ValueError, TypeError):
return jsonify(error="page_id must be provided in request query"), 400
pinned = "pinned" in request.args
disabled = "disabled" in request.args
enabled = "enabled" in request.args
if pinned:
alias_filter = "pinned"
elif disabled:
alias_filter = "disabled"
elif enabled:
alias_filter = "enabled"
else:
alias_filter = None
query = None
data = request.get_json(silent=True)
if data:
query = data.get("query")
alias_infos: [AliasInfo] = get_alias_infos_with_pagination_v3(
user, page_id=page_id, query=query, alias_filter=alias_filter
)
return (
jsonify(
aliases=[serialize_alias_info_v2(alias_info) for alias_info in alias_infos]
), ),
200, 200,
) )
@api_bp.route("/aliases/<int:alias_id>", methods=["DELETE"]) @api_bp.route("/aliases/<int:alias_id>", methods=["DELETE"])
@cross_origin() @require_api_auth
@verify_api_key
def delete_alias(alias_id): def delete_alias(alias_id):
""" """
Delete alias Delete alias
@ -71,20 +156,18 @@ def delete_alias(alias_id):
""" """
user = g.user user = g.user
gen_email = GenEmail.get(alias_id) alias = Alias.get(alias_id)
if gen_email.user_id != user.id: if not alias or alias.user_id != user.id:
return jsonify(error="Forbidden"), 403 return jsonify(error="Forbidden"), 403
GenEmail.delete(alias_id) alias_utils.delete_alias(alias, user, AliasDeleteReason.ManualAction)
db.session.commit()
return jsonify(deleted=True), 200 return jsonify(deleted=True), 200
@api_bp.route("/aliases/<int:alias_id>/toggle", methods=["POST"]) @api_bp.route("/aliases/<int:alias_id>/toggle", methods=["POST"])
@cross_origin() @require_api_auth
@verify_api_key
def toggle_alias(alias_id): def toggle_alias(alias_id):
""" """
Enable/disable alias Enable/disable alias
@ -97,20 +180,20 @@ def toggle_alias(alias_id):
""" """
user = g.user user = g.user
gen_email: GenEmail = GenEmail.get(alias_id) alias: Alias = Alias.get(alias_id)
if gen_email.user_id != user.id: if not alias or alias.user_id != user.id:
return jsonify(error="Forbidden"), 403 return jsonify(error="Forbidden"), 403
gen_email.enabled = not gen_email.enabled alias_utils.change_alias_status(alias, enabled=not alias.enabled)
db.session.commit() LOG.i(f"User {user} changed alias {alias} enabled status to {alias.enabled}")
Session.commit()
return jsonify(enabled=gen_email.enabled), 200 return jsonify(enabled=alias.enabled), 200
@api_bp.route("/aliases/<int:alias_id>/activities") @api_bp.route("/aliases/<int:alias_id>/activities")
@cross_origin() @require_api_auth
@verify_api_key
def get_alias_activities(alias_id): def get_alias_activities(alias_id):
""" """
Get aliases Get aliases
@ -121,7 +204,8 @@ def get_alias_activities(alias_id):
- from - from
- to - to
- timestamp - timestamp
- action: forward|reply|block - action: forward|reply|block|bounced
- reverse_alias
""" """
user = g.user user = g.user
@ -130,23 +214,27 @@ def get_alias_activities(alias_id):
except (ValueError, TypeError): except (ValueError, TypeError):
return jsonify(error="page_id must be provided in request query"), 400 return jsonify(error="page_id must be provided in request query"), 400
gen_email: GenEmail = GenEmail.get(alias_id) alias: Alias = Alias.get(alias_id)
if gen_email.user_id != user.id: if not alias or alias.user_id != user.id:
return jsonify(error="Forbidden"), 403 return jsonify(error="Forbidden"), 403
alias_logs = get_alias_log(gen_email, page_id) alias_logs = get_alias_log(alias, page_id)
activities = [] activities = []
for alias_log in alias_logs: for alias_log in alias_logs:
activity = {"timestamp": alias_log.when.timestamp} activity = {
"timestamp": alias_log.when.timestamp,
"reverse_alias": alias_log.reverse_alias,
"reverse_alias_address": alias_log.contact.reply_email,
}
if alias_log.is_reply: if alias_log.is_reply:
activity["from"] = alias_log.alias activity["from"] = alias_log.alias
activity["to"] = alias_log.website_from or alias_log.website_email activity["to"] = alias_log.website_email
activity["action"] = "reply" activity["action"] = "reply"
else: else:
activity["to"] = alias_log.alias activity["to"] = alias_log.alias
activity["from"] = alias_log.website_from or alias_log.website_email activity["from"] = alias_log.website_email
if alias_log.bounced: if alias_log.bounced:
activity["action"] = "bounced" activity["action"] = "bounced"
@ -157,4 +245,235 @@ def get_alias_activities(alias_id):
activities.append(activity) activities.append(activity)
return (jsonify(activities=activities), 200) return jsonify(activities=activities), 200
@api_bp.route("/aliases/<int:alias_id>", methods=["PUT", "PATCH"])
@require_api_auth
def update_alias(alias_id):
"""
Update alias note
Input:
alias_id: in url
note (optional): in body
name (optional): in body
mailbox_id (optional): in body
disable_pgp (optional): in body
Output:
200
"""
data = request.get_json()
if not data:
return jsonify(error="request body cannot be empty"), 400
user = g.user
alias: Alias = Alias.get(alias_id)
if not alias or alias.user_id != user.id:
return jsonify(error="Forbidden"), 403
changed = False
if "note" in data:
new_note = data.get("note")
alias.note = new_note
changed = True
if "mailbox_id" in data:
mailbox_id = int(data.get("mailbox_id"))
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != user.id or not mailbox.verified:
return jsonify(error="Forbidden"), 400
alias.mailbox_id = mailbox_id
changed = True
if "mailbox_ids" in data:
mailbox_ids = [int(m_id) for m_id in data.get("mailbox_ids")]
mailboxes: [Mailbox] = []
# check if all mailboxes belong to user
for mailbox_id in mailbox_ids:
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != user.id or not mailbox.verified:
return jsonify(error="Forbidden"), 400
mailboxes.append(mailbox)
if not mailboxes:
return jsonify(error="Must choose at least one mailbox"), 400
# <<< update alias mailboxes >>>
# first remove all existing alias-mailboxes links
AliasMailbox.filter_by(alias_id=alias.id).delete()
Session.flush()
# then add all new mailboxes
for i, mailbox in enumerate(mailboxes):
if i == 0:
alias.mailbox_id = mailboxes[0].id
else:
AliasMailbox.create(alias_id=alias.id, mailbox_id=mailbox.id)
# <<< END update alias mailboxes >>>
changed = True
if "name" in data:
# to make sure alias name doesn't contain linebreak
new_name = data.get("name")
if new_name and len(new_name) > 128:
return jsonify(error="Name can't be longer than 128 characters"), 400
if new_name:
new_name = new_name.replace("\n", "")
alias.name = new_name
changed = True
if "disable_pgp" in data:
alias.disable_pgp = data.get("disable_pgp")
changed = True
if "pinned" in data:
alias.pinned = data.get("pinned")
changed = True
if changed:
Session.commit()
return jsonify(ok=True), 200
@api_bp.route("/aliases/<int:alias_id>", methods=["GET"])
@require_api_auth
def get_alias(alias_id):
"""
Get alias
Input:
alias_id: in url
Output:
Alias info, same as in get_aliases
"""
user = g.user
alias: Alias = Alias.get(alias_id)
if not alias:
return jsonify(error="Unknown error"), 400
if alias.user_id != user.id:
return jsonify(error="Forbidden"), 403
return jsonify(**serialize_alias_info_v2(get_alias_info_v2(alias))), 200
@api_bp.route("/aliases/<int:alias_id>/contacts")
@require_api_auth
def get_alias_contacts_route(alias_id):
"""
Get alias contacts
Input:
page_id: in query
Output:
- contacts: list of contacts:
- creation_date
- creation_timestamp
- last_email_sent_date
- last_email_sent_timestamp
- contact
- reverse_alias
"""
user = g.user
try:
page_id = int(request.args.get("page_id"))
except (ValueError, TypeError):
return jsonify(error="page_id must be provided in request query"), 400
alias: Alias = Alias.get(alias_id)
if not alias:
return jsonify(error="No such alias"), 404
if alias.user_id != user.id:
return jsonify(error="Forbidden"), 403
contacts = get_alias_contacts(alias, page_id)
return jsonify(contacts=contacts), 200
@api_bp.route("/aliases/<int:alias_id>/contacts", methods=["POST"])
@require_api_auth
def create_contact_route(alias_id):
"""
Create contact for an alias
Input:
alias_id: in url
contact: in body
Output:
201 if success
409 if contact already added
"""
data = request.get_json()
if not data:
return jsonify(error="request body cannot be empty"), 400
alias: Alias = Alias.get(alias_id)
if alias.user_id != g.user.id:
return jsonify(error="Forbidden"), 403
contact_address = data.get("contact")
try:
contact = create_contact(g.user, alias, contact_address)
except ErrContactErrorUpgradeNeeded as err:
return jsonify(error=err.error_for_user()), 403
except (ErrAddressInvalid, CannotCreateContactForReverseAlias) as err:
return jsonify(error=err.error_for_user()), 400
except ErrContactAlreadyExists as err:
return jsonify(**serialize_contact(err.contact, existed=True)), 200
return jsonify(**serialize_contact(contact)), 201
@api_bp.route("/contacts/<int:contact_id>", methods=["DELETE"])
@require_api_auth
def delete_contact(contact_id):
"""
Delete contact
Input:
contact_id: in url
Output:
200
"""
user = g.user
contact = Contact.get(contact_id)
if not contact or contact.alias.user_id != user.id:
return jsonify(error="Forbidden"), 403
Contact.delete(contact_id)
Session.commit()
return jsonify(deleted=True), 200
@api_bp.route("/contacts/<int:contact_id>/toggle", methods=["POST"])
@require_api_auth
def toggle_contact(contact_id):
"""
Block/Unblock contact
Input:
contact_id: in url
Output:
200
"""
user = g.user
contact = Contact.get(contact_id)
if not contact or contact.alias.user_id != user.id:
return jsonify(error="Forbidden"), 403
contact.block_forward = not contact.block_forward
Session.commit()
return jsonify(block_forward=contact.block_forward), 200

View File

@ -1,105 +1,28 @@
import tldextract
from flask import jsonify, request, g from flask import jsonify, request, g
from flask_cors import cross_origin
from sqlalchemy import desc from sqlalchemy import desc
from app.api.base import api_bp, verify_api_key from app.alias_suffix import get_alias_suffixes
from app.config import ALIAS_DOMAINS, DISABLE_ALIAS_SUFFIX from app.api.base import api_bp, require_api_auth
from app.extensions import db from app.db import Session
from app.log import LOG from app.log import LOG
from app.models import AliasUsedOn, GenEmail, User from app.models import AliasUsedOn, Alias, User
from app.utils import convert_to_id, random_word from app.utils import convert_to_id
@api_bp.route("/alias/options") @api_bp.route("/v4/alias/options")
@cross_origin() @require_api_auth
@verify_api_key def options_v4():
def options():
"""
Return what options user has when creating new alias.
Input:
a valid api-key in "Authentication" header and
optional "hostname" in args
Output: cf README
optional recommendation:
optional custom
can_create_custom: boolean
existing: array of existing aliases
"""
LOG.error("/v2/alias/options should be used instead")
user = g.user
hostname = request.args.get("hostname")
ret = {
"existing": [ge.email for ge in GenEmail.query.filter_by(user_id=user.id)],
"can_create_custom": user.can_create_new_alias(),
}
# recommendation alias if exist
if hostname:
# put the latest used alias first
q = (
db.session.query(AliasUsedOn, GenEmail, User)
.filter(
AliasUsedOn.gen_email_id == GenEmail.id,
GenEmail.user_id == user.id,
AliasUsedOn.hostname == hostname,
)
.order_by(desc(AliasUsedOn.created_at))
)
r = q.first()
if r:
_, alias, _ = r
LOG.d("found alias %s %s %s", alias, hostname, user)
ret["recommendation"] = {"alias": alias.email, "hostname": hostname}
# custom alias suggestion and suffix
ret["custom"] = {}
if hostname:
# keep only the domain name of hostname, ignore TLD and subdomain
# for ex www.groupon.com -> groupon
domain_name = hostname
if "." in hostname:
parts = hostname.split(".")
domain_name = parts[-2]
domain_name = convert_to_id(domain_name)
ret["custom"]["suggestion"] = domain_name
else:
ret["custom"]["suggestion"] = ""
ret["custom"]["suffixes"] = []
# maybe better to make sure the suffix is never used before
# but this is ok as there's a check when creating a new custom alias
for domain in ALIAS_DOMAINS:
if DISABLE_ALIAS_SUFFIX:
ret["custom"]["suffixes"].append(f"@{domain}")
else:
ret["custom"]["suffixes"].append(f".{random_word()}@{domain}")
for custom_domain in user.verified_custom_domains():
ret["custom"]["suffixes"].append("@" + custom_domain.domain)
# custom domain should be put first
ret["custom"]["suffixes"] = list(reversed(ret["custom"]["suffixes"]))
return jsonify(ret)
@api_bp.route("/v2/alias/options")
@cross_origin()
@verify_api_key
def options_v2():
""" """
Return what options user has when creating new alias. Return what options user has when creating new alias.
Same as v3 but return time-based signed-suffix in addition to suffix. To be used with /v2/alias/custom/new
Input: Input:
a valid api-key in "Authentication" header and a valid api-key in "Authentication" header and
optional "hostname" in args optional "hostname" in args
Output: cf README Output: cf README
can_create: bool can_create: bool
suffixes: [str] suffixes: [[suffix, signed_suffix]]
prefix_suggestion: str prefix_suggestion: str
existing: [str]
recommendation: Optional dict recommendation: Optional dict
alias: str alias: str
hostname: str hostname: str
@ -110,9 +33,6 @@ def options_v2():
hostname = request.args.get("hostname") hostname = request.args.get("hostname")
ret = { ret = {
"existing": [
ge.email for ge in GenEmail.query.filter_by(user_id=user.id, enabled=True)
],
"can_create": user.can_create_new_alias(), "can_create": user.can_create_new_alias(),
"suffixes": [], "suffixes": [],
"prefix_suggestion": "", "prefix_suggestion": "",
@ -122,10 +42,10 @@ def options_v2():
if hostname: if hostname:
# put the latest used alias first # put the latest used alias first
q = ( q = (
db.session.query(AliasUsedOn, GenEmail, User) Session.query(AliasUsedOn, Alias, User)
.filter( .filter(
AliasUsedOn.gen_email_id == GenEmail.id, AliasUsedOn.alias_id == Alias.id,
GenEmail.user_id == user.id, Alias.user_id == user.id,
AliasUsedOn.hostname == hostname, AliasUsedOn.hostname == hostname,
) )
.order_by(desc(AliasUsedOn.created_at)) .order_by(desc(AliasUsedOn.created_at))
@ -141,25 +61,93 @@ def options_v2():
if hostname: if hostname:
# keep only the domain name of hostname, ignore TLD and subdomain # keep only the domain name of hostname, ignore TLD and subdomain
# for ex www.groupon.com -> groupon # for ex www.groupon.com -> groupon
domain_name = hostname ext = tldextract.extract(hostname)
if "." in hostname: prefix_suggestion = ext.domain
parts = hostname.split(".") prefix_suggestion = convert_to_id(prefix_suggestion)
domain_name = parts[-2] ret["prefix_suggestion"] = prefix_suggestion
domain_name = convert_to_id(domain_name)
ret["prefix_suggestion"] = domain_name
# maybe better to make sure the suffix is never used before suffixes = get_alias_suffixes(user)
# but this is ok as there's a check when creating a new custom alias
for domain in ALIAS_DOMAINS:
if DISABLE_ALIAS_SUFFIX:
ret["suffixes"].append(f"@{domain}")
else:
ret["suffixes"].append(f".{random_word()}@{domain}")
for custom_domain in user.verified_custom_domains():
ret["suffixes"].append("@" + custom_domain.domain)
# custom domain should be put first # custom domain should be put first
ret["suffixes"] = list(reversed(ret["suffixes"])) ret["suffixes"] = list([suffix.suffix, suffix.signed_suffix] for suffix in suffixes)
return jsonify(ret)
@api_bp.route("/v5/alias/options")
@require_api_auth
def options_v5():
"""
Return what options user has when creating new alias.
Same as v4 but uses a better format. To be used with /v2/alias/custom/new
Input:
a valid api-key in "Authentication" header and
optional "hostname" in args
Output: cf README
can_create: bool
suffixes: [
{
suffix: "suffix",
signed_suffix: "signed_suffix",
is_custom: true,
is_premium: false
}
]
prefix_suggestion: str
recommendation: Optional dict
alias: str
hostname: str
"""
user = g.user
hostname = request.args.get("hostname")
ret = {
"can_create": user.can_create_new_alias(),
"suffixes": [],
"prefix_suggestion": "",
}
# recommendation alias if exist
if hostname:
# put the latest used alias first
q = (
Session.query(AliasUsedOn, Alias, User)
.filter(
AliasUsedOn.alias_id == Alias.id,
Alias.user_id == user.id,
AliasUsedOn.hostname == hostname,
)
.order_by(desc(AliasUsedOn.created_at))
)
r = q.first()
if r:
_, alias, _ = r
LOG.d("found alias %s %s %s", alias, hostname, user)
ret["recommendation"] = {"alias": alias.email, "hostname": hostname}
# custom alias suggestion and suffix
if hostname:
# keep only the domain name of hostname, ignore TLD and subdomain
# for ex www.groupon.com -> groupon
ext = tldextract.extract(hostname)
prefix_suggestion = ext.domain
prefix_suggestion = convert_to_id(prefix_suggestion)
ret["prefix_suggestion"] = prefix_suggestion
suffixes = get_alias_suffixes(user)
# custom domain should be put first
ret["suffixes"] = [
{
"suffix": suffix.suffix,
"signed_suffix": suffix.signed_suffix,
"is_custom": suffix.is_custom,
"is_premium": suffix.is_premium,
}
for suffix in suffixes
]
return jsonify(ret) return jsonify(ret)

576
app/api/views/apple.py Normal file
View File

@ -0,0 +1,576 @@
from typing import Optional
import arrow
import requests
from flask import g
from flask import jsonify
from flask import request
from requests import RequestException
from app.api.base import api_bp, require_api_auth
from app.config import APPLE_API_SECRET, MACAPP_APPLE_API_SECRET
from app.subscription_webhook import execute_subscription_webhook
from app.db import Session
from app.log import LOG
from app.models import PlanEnum, AppleSubscription
_MONTHLY_PRODUCT_ID = "io.simplelogin.ios_app.subscription.premium.monthly"
_YEARLY_PRODUCT_ID = "io.simplelogin.ios_app.subscription.premium.yearly"
# SL Mac app used to be in SL account
_MACAPP_MONTHLY_PRODUCT_ID = "io.simplelogin.macapp.subscription.premium.monthly"
_MACAPP_YEARLY_PRODUCT_ID = "io.simplelogin.macapp.subscription.premium.yearly"
# SL Mac app is moved to Proton account
_MACAPP_MONTHLY_PRODUCT_ID_NEW = "me.proton.simplelogin.macos.premium.monthly"
_MACAPP_YEARLY_PRODUCT_ID_NEW = "me.proton.simplelogin.macos.premium.yearly"
# Apple API URL
_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"
_PROD_URL = "https://buy.itunes.apple.com/verifyReceipt"
@api_bp.route("/apple/process_payment", methods=["POST"])
@require_api_auth
def apple_process_payment():
"""
Process payment
Input:
receipt_data: in body
(optional) is_macapp: in body
Output:
200 of the payment is successful, i.e. user is upgraded to premium
"""
user = g.user
LOG.d("request for /apple/process_payment from %s", user)
data = request.get_json()
receipt_data = data.get("receipt_data")
is_macapp = "is_macapp" in data and data["is_macapp"] is True
if is_macapp:
LOG.d("Use Macapp secret")
password = MACAPP_APPLE_API_SECRET
else:
password = APPLE_API_SECRET
apple_sub = verify_receipt(receipt_data, user, password)
if apple_sub:
execute_subscription_webhook(user)
return jsonify(ok=True), 200
return jsonify(error="Processing failed"), 400
@api_bp.route("/apple/update_notification", methods=["GET", "POST"])
def apple_update_notification():
"""
The "Subscription Status URL" to receive update notifications from Apple
"""
# request.json looks like this
# will use unified_receipt.latest_receipt_info and NOT latest_expired_receipt_info
# more info on https://developer.apple.com/documentation/appstoreservernotifications/responsebody
# {
# "unified_receipt": {
# "latest_receipt": "long string",
# "pending_renewal_info": [
# {
# "is_in_billing_retry_period": "0",
# "auto_renew_status": "0",
# "original_transaction_id": "1000000654277043",
# "product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "expiration_intent": "1",
# "auto_renew_product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# }
# ],
# "environment": "Sandbox",
# "status": 0,
# "latest_receipt_info": [
# {
# "expires_date_pst": "2020-04-20 21:11:57 America/Los_Angeles",
# "purchase_date": "2020-04-21 03:11:57 Etc/GMT",
# "purchase_date_ms": "1587438717000",
# "original_purchase_date_ms": "1587420715000",
# "transaction_id": "1000000654329911",
# "original_transaction_id": "1000000654277043",
# "quantity": "1",
# "expires_date_ms": "1587442317000",
# "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles",
# "product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "subscription_group_identifier": "20624274",
# "web_order_line_item_id": "1000000051891577",
# "expires_date": "2020-04-21 04:11:57 Etc/GMT",
# "is_in_intro_offer_period": "false",
# "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT",
# "purchase_date_pst": "2020-04-20 20:11:57 America/Los_Angeles",
# "is_trial_period": "false",
# },
# {
# "expires_date_pst": "2020-04-20 20:11:57 America/Los_Angeles",
# "purchase_date": "2020-04-21 02:11:57 Etc/GMT",
# "purchase_date_ms": "1587435117000",
# "original_purchase_date_ms": "1587420715000",
# "transaction_id": "1000000654313889",
# "original_transaction_id": "1000000654277043",
# "quantity": "1",
# "expires_date_ms": "1587438717000",
# "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles",
# "product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "subscription_group_identifier": "20624274",
# "web_order_line_item_id": "1000000051890729",
# "expires_date": "2020-04-21 03:11:57 Etc/GMT",
# "is_in_intro_offer_period": "false",
# "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT",
# "purchase_date_pst": "2020-04-20 19:11:57 America/Los_Angeles",
# "is_trial_period": "false",
# },
# {
# "expires_date_pst": "2020-04-20 19:11:54 America/Los_Angeles",
# "purchase_date": "2020-04-21 01:11:54 Etc/GMT",
# "purchase_date_ms": "1587431514000",
# "original_purchase_date_ms": "1587420715000",
# "transaction_id": "1000000654300800",
# "original_transaction_id": "1000000654277043",
# "quantity": "1",
# "expires_date_ms": "1587435114000",
# "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles",
# "product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "subscription_group_identifier": "20624274",
# "web_order_line_item_id": "1000000051890161",
# "expires_date": "2020-04-21 02:11:54 Etc/GMT",
# "is_in_intro_offer_period": "false",
# "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT",
# "purchase_date_pst": "2020-04-20 18:11:54 America/Los_Angeles",
# "is_trial_period": "false",
# },
# {
# "expires_date_pst": "2020-04-20 18:11:54 America/Los_Angeles",
# "purchase_date": "2020-04-21 00:11:54 Etc/GMT",
# "purchase_date_ms": "1587427914000",
# "original_purchase_date_ms": "1587420715000",
# "transaction_id": "1000000654293615",
# "original_transaction_id": "1000000654277043",
# "quantity": "1",
# "expires_date_ms": "1587431514000",
# "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles",
# "product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "subscription_group_identifier": "20624274",
# "web_order_line_item_id": "1000000051889539",
# "expires_date": "2020-04-21 01:11:54 Etc/GMT",
# "is_in_intro_offer_period": "false",
# "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT",
# "purchase_date_pst": "2020-04-20 17:11:54 America/Los_Angeles",
# "is_trial_period": "false",
# },
# {
# "expires_date_pst": "2020-04-20 17:11:54 America/Los_Angeles",
# "purchase_date": "2020-04-20 23:11:54 Etc/GMT",
# "purchase_date_ms": "1587424314000",
# "original_purchase_date_ms": "1587420715000",
# "transaction_id": "1000000654285464",
# "original_transaction_id": "1000000654277043",
# "quantity": "1",
# "expires_date_ms": "1587427914000",
# "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles",
# "product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "subscription_group_identifier": "20624274",
# "web_order_line_item_id": "1000000051888827",
# "expires_date": "2020-04-21 00:11:54 Etc/GMT",
# "is_in_intro_offer_period": "false",
# "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT",
# "purchase_date_pst": "2020-04-20 16:11:54 America/Los_Angeles",
# "is_trial_period": "false",
# },
# {
# "expires_date_pst": "2020-04-20 16:11:54 America/Los_Angeles",
# "purchase_date": "2020-04-20 22:11:54 Etc/GMT",
# "purchase_date_ms": "1587420714000",
# "original_purchase_date_ms": "1587420715000",
# "transaction_id": "1000000654277043",
# "original_transaction_id": "1000000654277043",
# "quantity": "1",
# "expires_date_ms": "1587424314000",
# "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles",
# "product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "subscription_group_identifier": "20624274",
# "web_order_line_item_id": "1000000051888825",
# "expires_date": "2020-04-20 23:11:54 Etc/GMT",
# "is_in_intro_offer_period": "false",
# "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT",
# "purchase_date_pst": "2020-04-20 15:11:54 America/Los_Angeles",
# "is_trial_period": "false",
# },
# ],
# },
# "auto_renew_status_change_date": "2020-04-21 04:11:33 Etc/GMT",
# "environment": "Sandbox",
# "auto_renew_status": "false",
# "auto_renew_status_change_date_pst": "2020-04-20 21:11:33 America/Los_Angeles",
# "latest_expired_receipt": "long string",
# "latest_expired_receipt_info": {
# "original_purchase_date_pst": "2020-04-20 15:11:55 America/Los_Angeles",
# "quantity": "1",
# "subscription_group_identifier": "20624274",
# "unique_vendor_identifier": "4C4DF6BA-DE2A-4737-9A68-5992338886DC",
# "original_purchase_date_ms": "1587420715000",
# "expires_date_formatted": "2020-04-21 04:11:57 Etc/GMT",
# "is_in_intro_offer_period": "false",
# "purchase_date_ms": "1587438717000",
# "expires_date_formatted_pst": "2020-04-20 21:11:57 America/Los_Angeles",
# "is_trial_period": "false",
# "item_id": "1508744966",
# "unique_identifier": "b55fc3dcc688e979115af0697a0195be78be7cbd",
# "original_transaction_id": "1000000654277043",
# "expires_date": "1587442317000",
# "transaction_id": "1000000654329911",
# "bvrs": "3",
# "web_order_line_item_id": "1000000051891577",
# "version_external_identifier": "834289833",
# "bid": "io.simplelogin.ios-app",
# "product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "purchase_date": "2020-04-21 03:11:57 Etc/GMT",
# "purchase_date_pst": "2020-04-20 20:11:57 America/Los_Angeles",
# "original_purchase_date": "2020-04-20 22:11:55 Etc/GMT",
# },
# "password": "22b9d5a110dd4344a1681631f1f95f55",
# "auto_renew_status_change_date_ms": "1587442293000",
# "auto_renew_product_id": "io.simplelogin.ios_app.subscription.premium.yearly",
# "notification_type": "DID_CHANGE_RENEWAL_STATUS",
# }
LOG.d("request for /api/apple/update_notification")
data = request.get_json()
if not (
data
and data.get("unified_receipt")
and data["unified_receipt"].get("latest_receipt_info")
):
LOG.d("Invalid data %s", data)
return jsonify(error="Empty Response"), 400
transactions = data["unified_receipt"]["latest_receipt_info"]
# dict of original_transaction_id and transaction
latest_transactions = {}
for transaction in transactions:
original_transaction_id = transaction["original_transaction_id"]
if not latest_transactions.get(original_transaction_id):
latest_transactions[original_transaction_id] = transaction
if (
transaction["expires_date_ms"]
> latest_transactions[original_transaction_id]["expires_date_ms"]
):
latest_transactions[original_transaction_id] = transaction
for original_transaction_id, transaction in latest_transactions.items():
expires_date = arrow.get(int(transaction["expires_date_ms"]) / 1000)
plan = (
PlanEnum.monthly
if transaction["product_id"]
in (
_MONTHLY_PRODUCT_ID,
_MACAPP_MONTHLY_PRODUCT_ID,
_MACAPP_MONTHLY_PRODUCT_ID_NEW,
)
else PlanEnum.yearly
)
apple_sub: AppleSubscription = AppleSubscription.get_by(
original_transaction_id=original_transaction_id
)
if apple_sub:
user = apple_sub.user
LOG.d(
"Update AppleSubscription for user %s, expired at %s, plan %s",
user,
expires_date,
plan,
)
apple_sub.receipt_data = data["unified_receipt"]["latest_receipt"]
apple_sub.expires_date = expires_date
apple_sub.plan = plan
apple_sub.product_id = transaction["product_id"]
Session.commit()
execute_subscription_webhook(user)
return jsonify(ok=True), 200
else:
LOG.w(
"No existing AppleSub for original_transaction_id %s",
original_transaction_id,
)
LOG.d("request data %s", data)
return jsonify(error="Processing failed"), 400
def verify_receipt(receipt_data, user, password) -> Optional[AppleSubscription]:
"""
Call https://buy.itunes.apple.com/verifyReceipt and create/update AppleSubscription table
Call the production URL for verifyReceipt first,
use sandbox URL if receive a 21007 status code.
Return AppleSubscription object if success
https://developer.apple.com/documentation/appstorereceipts/verifyreceipt
"""
LOG.d("start verify_receipt")
try:
r = requests.post(
_PROD_URL, json={"receipt-data": receipt_data, "password": password}
)
except RequestException:
LOG.w("cannot call Apple server %s", _PROD_URL)
return None
if r.status_code >= 500:
LOG.w("Apple server error, response:%s %s", r, r.content)
return None
if r.json() == {"status": 21007}:
# try sandbox_url
LOG.w("Use the sandbox url instead")
r = requests.post(
_SANDBOX_URL,
json={"receipt-data": receipt_data, "password": password},
)
data = r.json()
# data has the following format
# {
# "status": 0,
# "environment": "Sandbox",
# "receipt": {
# "receipt_type": "ProductionSandbox",
# "adam_id": 0,
# "app_item_id": 0,
# "bundle_id": "io.simplelogin.ios-app",
# "application_version": "2",
# "download_id": 0,
# "version_external_identifier": 0,
# "receipt_creation_date": "2020-04-18 16:36:34 Etc/GMT",
# "receipt_creation_date_ms": "1587227794000",
# "receipt_creation_date_pst": "2020-04-18 09:36:34 America/Los_Angeles",
# "request_date": "2020-04-18 16:46:36 Etc/GMT",
# "request_date_ms": "1587228396496",
# "request_date_pst": "2020-04-18 09:46:36 America/Los_Angeles",
# "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
# "original_purchase_date_ms": "1375340400000",
# "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
# "original_application_version": "1.0",
# "in_app": [
# {
# "quantity": "1",
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
# "transaction_id": "1000000653584474",
# "original_transaction_id": "1000000653584474",
# "purchase_date": "2020-04-18 16:27:42 Etc/GMT",
# "purchase_date_ms": "1587227262000",
# "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles",
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
# "original_purchase_date_ms": "1587227264000",
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
# "expires_date": "2020-04-18 16:32:42 Etc/GMT",
# "expires_date_ms": "1587227562000",
# "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
# "web_order_line_item_id": "1000000051847459",
# "is_trial_period": "false",
# "is_in_intro_offer_period": "false",
# },
# {
# "quantity": "1",
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
# "transaction_id": "1000000653584861",
# "original_transaction_id": "1000000653584474",
# "purchase_date": "2020-04-18 16:32:42 Etc/GMT",
# "purchase_date_ms": "1587227562000",
# "purchase_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
# "original_purchase_date_ms": "1587227264000",
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
# "expires_date": "2020-04-18 16:37:42 Etc/GMT",
# "expires_date_ms": "1587227862000",
# "expires_date_pst": "2020-04-18 09:37:42 America/Los_Angeles",
# "web_order_line_item_id": "1000000051847461",
# "is_trial_period": "false",
# "is_in_intro_offer_period": "false",
# },
# ],
# },
# "latest_receipt_info": [
# {
# "quantity": "1",
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
# "transaction_id": "1000000653584474",
# "original_transaction_id": "1000000653584474",
# "purchase_date": "2020-04-18 16:27:42 Etc/GMT",
# "purchase_date_ms": "1587227262000",
# "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles",
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
# "original_purchase_date_ms": "1587227264000",
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
# "expires_date": "2020-04-18 16:32:42 Etc/GMT",
# "expires_date_ms": "1587227562000",
# "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
# "web_order_line_item_id": "1000000051847459",
# "is_trial_period": "false",
# "is_in_intro_offer_period": "false",
# "subscription_group_identifier": "20624274",
# },
# {
# "quantity": "1",
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
# "transaction_id": "1000000653584861",
# "original_transaction_id": "1000000653584474",
# "purchase_date": "2020-04-18 16:32:42 Etc/GMT",
# "purchase_date_ms": "1587227562000",
# "purchase_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
# "original_purchase_date_ms": "1587227264000",
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
# "expires_date": "2020-04-18 16:37:42 Etc/GMT",
# "expires_date_ms": "1587227862000",
# "expires_date_pst": "2020-04-18 09:37:42 America/Los_Angeles",
# "web_order_line_item_id": "1000000051847461",
# "is_trial_period": "false",
# "is_in_intro_offer_period": "false",
# "subscription_group_identifier": "20624274",
# },
# {
# "quantity": "1",
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
# "transaction_id": "1000000653585235",
# "original_transaction_id": "1000000653584474",
# "purchase_date": "2020-04-18 16:38:16 Etc/GMT",
# "purchase_date_ms": "1587227896000",
# "purchase_date_pst": "2020-04-18 09:38:16 America/Los_Angeles",
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
# "original_purchase_date_ms": "1587227264000",
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
# "expires_date": "2020-04-18 16:43:16 Etc/GMT",
# "expires_date_ms": "1587228196000",
# "expires_date_pst": "2020-04-18 09:43:16 America/Los_Angeles",
# "web_order_line_item_id": "1000000051847500",
# "is_trial_period": "false",
# "is_in_intro_offer_period": "false",
# "subscription_group_identifier": "20624274",
# },
# {
# "quantity": "1",
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
# "transaction_id": "1000000653585760",
# "original_transaction_id": "1000000653584474",
# "purchase_date": "2020-04-18 16:44:25 Etc/GMT",
# "purchase_date_ms": "1587228265000",
# "purchase_date_pst": "2020-04-18 09:44:25 America/Los_Angeles",
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
# "original_purchase_date_ms": "1587227264000",
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
# "expires_date": "2020-04-18 16:49:25 Etc/GMT",
# "expires_date_ms": "1587228565000",
# "expires_date_pst": "2020-04-18 09:49:25 America/Los_Angeles",
# "web_order_line_item_id": "1000000051847566",
# "is_trial_period": "false",
# "is_in_intro_offer_period": "false",
# "subscription_group_identifier": "20624274",
# },
# ],
# "latest_receipt": "very long string",
# "pending_renewal_info": [
# {
# "auto_renew_product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
# "original_transaction_id": "1000000653584474",
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
# "auto_renew_status": "1",
# }
# ],
# }
if data["status"] != 0:
LOG.e(
"verifyReceipt status !=0, probably invalid receipt. User %s, data %s",
user,
data,
)
return None
# use responseBody.Latest_receipt_info and not responseBody.Receipt.In_app
# as recommended on https://developer.apple.com/documentation/appstorereceipts/responsebody/receipt/in_app
# each item in data["latest_receipt_info"] has the following format
# {
# "quantity": "1",
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
# "transaction_id": "1000000653584474",
# "original_transaction_id": "1000000653584474",
# "purchase_date": "2020-04-18 16:27:42 Etc/GMT",
# "purchase_date_ms": "1587227262000",
# "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles",
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
# "original_purchase_date_ms": "1587227264000",
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
# "expires_date": "2020-04-18 16:32:42 Etc/GMT",
# "expires_date_ms": "1587227562000",
# "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
# "web_order_line_item_id": "1000000051847459",
# "is_trial_period": "false",
# "is_in_intro_offer_period": "false",
# }
transactions = data.get("latest_receipt_info")
if not transactions:
LOG.i("Empty transactions in data %s", data)
return None
latest_transaction = max(transactions, key=lambda t: int(t["expires_date_ms"]))
original_transaction_id = latest_transaction["original_transaction_id"]
expires_date = arrow.get(int(latest_transaction["expires_date_ms"]) / 1000)
plan = (
PlanEnum.monthly
if latest_transaction["product_id"]
in (
_MONTHLY_PRODUCT_ID,
_MACAPP_MONTHLY_PRODUCT_ID,
_MACAPP_MONTHLY_PRODUCT_ID_NEW,
)
else PlanEnum.yearly
)
apple_sub: AppleSubscription = AppleSubscription.get_by(user_id=user.id)
if apple_sub:
LOG.d(
"Update AppleSubscription for user %s, expired at %s (%s), plan %s",
user,
expires_date,
expires_date.humanize(),
plan,
)
apple_sub.receipt_data = receipt_data
apple_sub.expires_date = expires_date
apple_sub.original_transaction_id = original_transaction_id
apple_sub.product_id = latest_transaction["product_id"]
apple_sub.plan = plan
else:
# the same original_transaction_id has been used on another account
if AppleSubscription.get_by(original_transaction_id=original_transaction_id):
LOG.e("Same Apple Sub has been used before, current user %s", user)
return None
LOG.d(
"Create new AppleSubscription for user %s, expired at %s, plan %s",
user,
expires_date,
plan,
)
apple_sub = AppleSubscription.create(
user_id=user.id,
receipt_data=receipt_data,
expires_date=expires_date,
original_transaction_id=original_transaction_id,
plan=plan,
product_id=latest_transaction["product_id"],
)
execute_subscription_webhook(user)
Session.commit()
return apple_sub

View File

@ -1,28 +1,33 @@
import random import secrets
import string
import facebook import facebook
import google.oauth2.credentials import google.oauth2.credentials
import googleapiclient.discovery import googleapiclient.discovery
from flask import jsonify, request from flask import jsonify, request
from flask_cors import cross_origin from flask_login import login_user
from itsdangerous import Signer from itsdangerous import Signer
from app import email_utils from app import email_utils
from app.api.base import api_bp from app.api.base import api_bp
from app.config import FLASK_SECRET, DISABLE_REGISTRATION from app.config import FLASK_SECRET, DISABLE_REGISTRATION
from app.dashboard.views.account_setting import send_reset_password_email
from app.db import Session
from app.email_utils import ( from app.email_utils import (
can_be_used_as_personal_email, email_can_be_used_as_mailbox,
email_already_used, personal_email_already_used,
send_email, send_email,
render, render,
) )
from app.extensions import db from app.events.auth_event import LoginEvent, RegisterEvent
from app.extensions import limiter
from app.log import LOG from app.log import LOG
from app.models import User, ApiKey, SocialAuth, AccountActivation from app.models import User, ApiKey, SocialAuth, AccountActivation
from app.utils import sanitize_email, canonicalize_email
@api_bp.route("/auth/login", methods=["POST"]) @api_bp.route("/auth/login", methods=["POST"])
@cross_origin() @limiter.limit("10/minute")
def auth_login(): def auth_login():
""" """
Authenticate user Authenticate user
@ -44,22 +49,39 @@ def auth_login():
if not data: if not data:
return jsonify(error="request body cannot be empty"), 400 return jsonify(error="request body cannot be empty"), 400
email = data.get("email")
password = data.get("password") password = data.get("password")
device = data.get("device") device = data.get("device")
user = User.filter_by(email=email).first() email = sanitize_email(data.get("email"))
canonical_email = canonicalize_email(data.get("email"))
user = User.get_by(email=email) or User.get_by(email=canonical_email)
if not user or not user.check_password(password): if not user or not user.check_password(password):
LoginEvent(LoginEvent.ActionType.failed, LoginEvent.Source.api).send()
return jsonify(error="Email or password incorrect"), 400 return jsonify(error="Email or password incorrect"), 400
elif user.disabled:
LoginEvent(LoginEvent.ActionType.disabled_login, LoginEvent.Source.api).send()
return jsonify(error="Account disabled"), 400
elif user.delete_on is not None:
LoginEvent(
LoginEvent.ActionType.scheduled_to_be_deleted, LoginEvent.Source.api
).send()
return jsonify(error="Account scheduled for deletion"), 400
elif not user.activated: elif not user.activated:
return jsonify(error="Account not activated"), 400 LoginEvent(LoginEvent.ActionType.not_activated, LoginEvent.Source.api).send()
return jsonify(error="Account not activated"), 422
elif user.fido_enabled():
# allow user who has TOTP enabled to continue using the mobile app
if not user.enable_otp:
return jsonify(error="Currently we don't support FIDO on mobile yet"), 403
LoginEvent(LoginEvent.ActionType.success, LoginEvent.Source.api).send()
return jsonify(**auth_payload(user, device)), 200 return jsonify(**auth_payload(user, device)), 200
@api_bp.route("/auth/register", methods=["POST"]) @api_bp.route("/auth/register", methods=["POST"])
@cross_origin() @limiter.limit("10/minute")
def auth_register(): def auth_register():
""" """
User signs up - will need to activate their account with an activation code. User signs up - will need to activate their account with an activation code.
@ -74,38 +96,49 @@ def auth_register():
if not data: if not data:
return jsonify(error="request body cannot be empty"), 400 return jsonify(error="request body cannot be empty"), 400
email = data.get("email") dirty_email = data.get("email")
email = canonicalize_email(dirty_email)
password = data.get("password") password = data.get("password")
if DISABLE_REGISTRATION: if DISABLE_REGISTRATION:
RegisterEvent(RegisterEvent.ActionType.failed, RegisterEvent.Source.api).send()
return jsonify(error="registration is closed"), 400 return jsonify(error="registration is closed"), 400
if not can_be_used_as_personal_email(email) or email_already_used(email): if not email_can_be_used_as_mailbox(email) or personal_email_already_used(email):
RegisterEvent(
RegisterEvent.ActionType.invalid_email, RegisterEvent.Source.api
).send()
return jsonify(error=f"cannot use {email} as personal inbox"), 400 return jsonify(error=f"cannot use {email} as personal inbox"), 400
if not password or len(password) < 8: if not password or len(password) < 8:
RegisterEvent(RegisterEvent.ActionType.failed, RegisterEvent.Source.api).send()
return jsonify(error="password too short"), 400 return jsonify(error="password too short"), 400
LOG.debug("create user %s", email) if len(password) > 100:
user = User.create(email=email, name="", password=password) RegisterEvent(RegisterEvent.ActionType.failed, RegisterEvent.Source.api).send()
db.session.flush() return jsonify(error="password too long"), 400
LOG.d("create user %s", email)
user = User.create(email=email, name=dirty_email, password=password)
Session.flush()
# create activation code # create activation code
code = "".join([str(random.randint(0, 9)) for _ in range(6)]) code = "".join([str(secrets.choice(string.digits)) for _ in range(6)])
AccountActivation.create(user_id=user.id, code=code) AccountActivation.create(user_id=user.id, code=code)
db.session.commit() Session.commit()
send_email( send_email(
email, email,
f"Just one more step to join SimpleLogin", "Just one more step to join SimpleLogin",
render("transactional/code-activation.txt", code=code), render("transactional/code-activation.txt.jinja2", user=user, code=code),
render("transactional/code-activation.html", code=code), render("transactional/code-activation.html", user=user, code=code),
) )
RegisterEvent(RegisterEvent.ActionType.success, RegisterEvent.Source.api).send()
return jsonify(msg="User needs to confirm their account"), 200 return jsonify(msg="User needs to confirm their account"), 200
@api_bp.route("/auth/activate", methods=["POST"]) @api_bp.route("/auth/activate", methods=["POST"])
@cross_origin() @limiter.limit("10/minute")
def auth_activate(): def auth_activate():
""" """
User enters the activation code to confirm their account. User enters the activation code to confirm their account.
@ -122,10 +155,11 @@ def auth_activate():
if not data: if not data:
return jsonify(error="request body cannot be empty"), 400 return jsonify(error="request body cannot be empty"), 400
email = data.get("email") email = sanitize_email(data.get("email"))
canonical_email = canonicalize_email(data.get("email"))
code = data.get("code") code = data.get("code")
user = User.get_by(email=email) user = User.get_by(email=email) or User.get_by(email=canonical_email)
# do not use a different message to avoid exposing existing email # do not use a different message to avoid exposing existing email
if not user or user.activated: if not user or user.activated:
@ -138,25 +172,25 @@ def auth_activate():
if account_activation.code != code: if account_activation.code != code:
# decrement nb tries # decrement nb tries
account_activation.tries -= 1 account_activation.tries -= 1
db.session.commit() Session.commit()
if account_activation.tries == 0: if account_activation.tries == 0:
AccountActivation.delete(account_activation.id) AccountActivation.delete(account_activation.id)
db.session.commit() Session.commit()
return jsonify(error="Too many wrong tries"), 410 return jsonify(error="Too many wrong tries"), 410
return jsonify(error="Wrong email or code"), 400 return jsonify(error="Wrong email or code"), 400
LOG.debug("activate user %s", user) LOG.d("activate user %s", user)
user.activated = True user.activated = True
AccountActivation.delete(account_activation.id) AccountActivation.delete(account_activation.id)
db.session.commit() Session.commit()
return jsonify(msg="Account is activated, user can login now"), 200 return jsonify(msg="Account is activated, user can login now"), 200
@api_bp.route("/auth/reactivate", methods=["POST"]) @api_bp.route("/auth/reactivate", methods=["POST"])
@cross_origin() @limiter.limit("10/minute")
def auth_reactivate(): def auth_reactivate():
""" """
User asks for another activation code User asks for another activation code
@ -170,8 +204,10 @@ def auth_reactivate():
if not data: if not data:
return jsonify(error="request body cannot be empty"), 400 return jsonify(error="request body cannot be empty"), 400
email = data.get("email") email = sanitize_email(data.get("email"))
user = User.get_by(email=email) canonical_email = canonicalize_email(data.get("email"))
user = User.get_by(email=email) or User.get_by(email=canonical_email)
# do not use a different message to avoid exposing existing email # do not use a different message to avoid exposing existing email
if not user or user.activated: if not user or user.activated:
@ -180,25 +216,25 @@ def auth_reactivate():
account_activation = AccountActivation.get_by(user_id=user.id) account_activation = AccountActivation.get_by(user_id=user.id)
if account_activation: if account_activation:
AccountActivation.delete(account_activation.id) AccountActivation.delete(account_activation.id)
db.session.commit() Session.commit()
# create activation code # create activation code
code = "".join([str(random.randint(0, 9)) for _ in range(6)]) code = "".join([str(secrets.choice(string.digits)) for _ in range(6)])
AccountActivation.create(user_id=user.id, code=code) AccountActivation.create(user_id=user.id, code=code)
db.session.commit() Session.commit()
send_email( send_email(
email, email,
f"Just one more step to join SimpleLogin", "Just one more step to join SimpleLogin",
render("transactional/code-activation.txt", code=code), render("transactional/code-activation.txt.jinja2", user=user, code=code),
render("transactional/code-activation.html", code=code), render("transactional/code-activation.html", user=user, code=code),
) )
return jsonify(msg="User needs to confirm their account"), 200 return jsonify(msg="User needs to confirm their account"), 200
@api_bp.route("/auth/facebook", methods=["POST"]) @api_bp.route("/auth/facebook", methods=["POST"])
@cross_origin() @limiter.limit("10/minute")
def auth_facebook(): def auth_facebook():
""" """
Authenticate user with Facebook Authenticate user with Facebook
@ -224,33 +260,35 @@ def auth_facebook():
graph = facebook.GraphAPI(access_token=facebook_token) graph = facebook.GraphAPI(access_token=facebook_token)
user_info = graph.get_object("me", fields="email,name") user_info = graph.get_object("me", fields="email,name")
email = user_info.get("email") email = sanitize_email(user_info.get("email"))
user = User.get_by(email=email) user = User.get_by(email=email)
if not user: if not user:
if DISABLE_REGISTRATION: if DISABLE_REGISTRATION:
return jsonify(error="registration is closed"), 400 return jsonify(error="registration is closed"), 400
if not can_be_used_as_personal_email(email) or email_already_used(email): if not email_can_be_used_as_mailbox(email) or personal_email_already_used(
email
):
return jsonify(error=f"cannot use {email} as personal inbox"), 400 return jsonify(error=f"cannot use {email} as personal inbox"), 400
LOG.d("create facebook user with %s", user_info) LOG.d("create facebook user with %s", user_info)
user = User.create(email=email.lower(), name=user_info["name"], activated=True) user = User.create(email=email, name=user_info["name"], activated=True)
db.session.commit() Session.commit()
email_utils.send_welcome_email(user) email_utils.send_welcome_email(user)
if not SocialAuth.get_by(user_id=user.id, social="facebook"): if not SocialAuth.get_by(user_id=user.id, social="facebook"):
SocialAuth.create(user_id=user.id, social="facebook") SocialAuth.create(user_id=user.id, social="facebook")
db.session.commit() Session.commit()
return jsonify(**auth_payload(user, device)), 200 return jsonify(**auth_payload(user, device)), 200
@api_bp.route("/auth/google", methods=["POST"]) @api_bp.route("/auth/google", methods=["POST"])
@cross_origin() @limiter.limit("10/minute")
def auth_google(): def auth_google():
""" """
Authenticate user with Facebook Authenticate user with Google
Input: Input:
google_token: Google access token google_token: Google access token
device: to create an ApiKey associated with this device device: to create an ApiKey associated with this device
@ -276,30 +314,32 @@ def auth_google():
build = googleapiclient.discovery.build("oauth2", "v2", credentials=cred) build = googleapiclient.discovery.build("oauth2", "v2", credentials=cred)
user_info = build.userinfo().get().execute() user_info = build.userinfo().get().execute()
email = user_info.get("email") email = sanitize_email(user_info.get("email"))
user = User.get_by(email=email) user = User.get_by(email=email)
if not user: if not user:
if DISABLE_REGISTRATION: if DISABLE_REGISTRATION:
return jsonify(error="registration is closed"), 400 return jsonify(error="registration is closed"), 400
if not can_be_used_as_personal_email(email) or email_already_used(email): if not email_can_be_used_as_mailbox(email) or personal_email_already_used(
email
):
return jsonify(error=f"cannot use {email} as personal inbox"), 400 return jsonify(error=f"cannot use {email} as personal inbox"), 400
LOG.d("create Google user with %s", user_info) LOG.d("create Google user with %s", user_info)
user = User.create(email=email.lower(), name="", activated=True) user = User.create(email=email, name="", activated=True)
db.session.commit() Session.commit()
email_utils.send_welcome_email(user) email_utils.send_welcome_email(user)
if not SocialAuth.get_by(user_id=user.id, social="google"): if not SocialAuth.get_by(user_id=user.id, social="google"):
SocialAuth.create(user_id=user.id, social="google") SocialAuth.create(user_id=user.id, social="google")
db.session.commit() Session.commit()
return jsonify(**auth_payload(user, device)), 200 return jsonify(**auth_payload(user, device)), 200
def auth_payload(user, device) -> dict: def auth_payload(user, device) -> dict:
ret = {"name": user.name, "mfa_enabled": user.enable_otp} ret = {"name": user.name or "", "email": user.email, "mfa_enabled": user.enable_otp}
# do not give api_key, user can only obtain api_key after OTP verification # do not give api_key, user can only obtain api_key after OTP verification
if user.enable_otp: if user.enable_otp:
@ -311,8 +351,38 @@ def auth_payload(user, device) -> dict:
if not api_key: if not api_key:
LOG.d("create new api key for %s and %s", user, device) LOG.d("create new api key for %s and %s", user, device)
api_key = ApiKey.create(user.id, device) api_key = ApiKey.create(user.id, device)
db.session.commit() Session.commit()
ret["mfa_key"] = None ret["mfa_key"] = None
ret["api_key"] = api_key.code ret["api_key"] = api_key.code
# so user is automatically logged in on the web
login_user(user)
return ret return ret
@api_bp.route("/auth/forgot_password", methods=["POST"])
@limiter.limit("2/minute")
def forgot_password():
"""
User forgot password
Input:
email
Output:
200 and a reset password email is sent to user
400 if email not exist
"""
data = request.get_json()
if not data or not data.get("email"):
return jsonify(error="request body must contain email"), 400
email = sanitize_email(data.get("email"))
canonical_email = canonicalize_email(data.get("email"))
user = User.get_by(email=email) or User.get_by(email=canonical_email)
if user:
send_reset_password_email(user)
return jsonify(ok=True)

View File

@ -1,17 +1,19 @@
import pyotp import pyotp
from flask import jsonify, request from flask import jsonify, request
from flask_cors import cross_origin from flask_login import login_user
from itsdangerous import Signer, BadSignature from itsdangerous import Signer
from app.api.base import api_bp from app.api.base import api_bp
from app.config import FLASK_SECRET from app.config import FLASK_SECRET
from app.extensions import db from app.db import Session
from app.email_utils import send_invalid_totp_login_email
from app.extensions import limiter
from app.log import LOG from app.log import LOG
from app.models import User, ApiKey from app.models import User, ApiKey
@api_bp.route("/auth/mfa", methods=["POST"]) @api_bp.route("/auth/mfa", methods=["POST"])
@cross_origin() @limiter.limit("10/minute")
def auth_mfa(): def auth_mfa():
""" """
Validate the OTP Token Validate the OTP Token
@ -23,7 +25,8 @@ def auth_mfa():
200 and user info containing: 200 and user info containing:
{ {
name: "John Wick", name: "John Wick",
api_key: "a long string" api_key: "a long string",
email: "user email"
} }
""" """
@ -52,17 +55,21 @@ def auth_mfa():
) )
totp = pyotp.TOTP(user.otp_secret) totp = pyotp.TOTP(user.otp_secret)
if not totp.verify(mfa_token): if not totp.verify(mfa_token, valid_window=2):
send_invalid_totp_login_email(user, "TOTP")
return jsonify(error="Wrong TOTP Token"), 400 return jsonify(error="Wrong TOTP Token"), 400
ret = {"name": user.name} ret = {"name": user.name or "", "email": user.email}
api_key = ApiKey.get_by(user_id=user.id, name=device) api_key = ApiKey.get_by(user_id=user.id, name=device)
if not api_key: if not api_key:
LOG.d("create new api key for %s and %s", user, device) LOG.d("create new api key for %s and %s", user, device)
api_key = ApiKey.create(user.id, device) api_key = ApiKey.create(user.id, device)
db.session.commit() Session.commit()
ret["api_key"] = api_key.code ret["api_key"] = api_key.code
# so user is logged in automatically on the web
login_user(user)
return jsonify(**ret), 200 return jsonify(**ret), 200

View File

@ -0,0 +1,126 @@
from flask import g, request
from flask import jsonify
from app.api.base import api_bp, require_api_auth
from app.db import Session
from app.models import CustomDomain, DomainDeletedAlias, Mailbox, DomainMailbox
def custom_domain_to_dict(custom_domain: CustomDomain):
return {
"id": custom_domain.id,
"domain_name": custom_domain.domain,
"is_verified": custom_domain.verified,
"nb_alias": custom_domain.nb_alias(),
"creation_date": custom_domain.created_at.format(),
"creation_timestamp": custom_domain.created_at.timestamp,
"catch_all": custom_domain.catch_all,
"name": custom_domain.name,
"random_prefix_generation": custom_domain.random_prefix_generation,
"mailboxes": [
{"id": mb.id, "email": mb.email} for mb in custom_domain.mailboxes
],
}
@api_bp.route("/custom_domains", methods=["GET"])
@require_api_auth
def get_custom_domains():
user = g.user
custom_domains = CustomDomain.filter_by(
user_id=user.id, is_sl_subdomain=False
).all()
return jsonify(custom_domains=[custom_domain_to_dict(cd) for cd in custom_domains])
@api_bp.route("/custom_domains/<int:custom_domain_id>/trash", methods=["GET"])
@require_api_auth
def get_custom_domain_trash(custom_domain_id: int):
user = g.user
custom_domain = CustomDomain.get(custom_domain_id)
if not custom_domain or custom_domain.user_id != user.id:
return jsonify(error="Forbidden"), 403
domain_deleted_aliases = DomainDeletedAlias.filter_by(
domain_id=custom_domain.id
).all()
return jsonify(
aliases=[
{
"alias": dda.email,
"deletion_timestamp": dda.created_at.timestamp,
}
for dda in domain_deleted_aliases
]
)
@api_bp.route("/custom_domains/<int:custom_domain_id>", methods=["PATCH"])
@require_api_auth
def update_custom_domain(custom_domain_id):
"""
Update alias note
Input:
custom_domain_id: in url
In body:
catch_all (optional): boolean
random_prefix_generation (optional): boolean
name (optional): in body
mailbox_ids (optional): array of mailbox_id
Output:
200
"""
data = request.get_json()
if not data:
return jsonify(error="request body cannot be empty"), 400
user = g.user
custom_domain: CustomDomain = CustomDomain.get(custom_domain_id)
if not custom_domain or custom_domain.user_id != user.id:
return jsonify(error="Forbidden"), 403
changed = False
if "catch_all" in data:
catch_all = data.get("catch_all")
custom_domain.catch_all = catch_all
changed = True
if "random_prefix_generation" in data:
random_prefix_generation = data.get("random_prefix_generation")
custom_domain.random_prefix_generation = random_prefix_generation
changed = True
if "name" in data:
name = data.get("name")
custom_domain.name = name
changed = True
if "mailbox_ids" in data:
mailbox_ids = [int(m_id) for m_id in data.get("mailbox_ids")]
if mailbox_ids:
# check if mailbox is not tempered with
mailboxes = []
for mailbox_id in mailbox_ids:
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != user.id or not mailbox.verified:
return jsonify(error="Forbidden"), 400
mailboxes.append(mailbox)
# first remove all existing domain-mailboxes links
DomainMailbox.filter_by(domain_id=custom_domain.id).delete()
Session.flush()
for mailbox in mailboxes:
DomainMailbox.create(domain_id=custom_domain.id, mailbox_id=mailbox.id)
changed = True
if changed:
Session.commit()
# refresh
custom_domain = CustomDomain.get(custom_domain_id)
return jsonify(custom_domain=custom_domain_to_dict(custom_domain)), 200

49
app/api/views/export.py Normal file
View File

@ -0,0 +1,49 @@
from flask import g
from flask import jsonify
from app.api.base import api_bp, require_api_auth
from app.models import Alias, Client, CustomDomain
from app.alias_utils import alias_export_csv
@api_bp.route("/export/data", methods=["GET"])
@require_api_auth
def export_data():
"""
Get user data
Output:
Alias, custom domain and app info
"""
user = g.user
data = {
"email": user.email,
"name": user.name,
"aliases": [],
"apps": [],
"custom_domains": [],
}
for alias in Alias.filter_by(user_id=user.id).all(): # type: Alias
data["aliases"].append(dict(email=alias.email, enabled=alias.enabled))
for custom_domain in CustomDomain.filter_by(user_id=user.id).all():
data["custom_domains"].append(custom_domain.domain)
for app in Client.filter_by(user_id=user.id): # type: Client
data["apps"].append(dict(name=app.name, home_url=app.home_url))
return jsonify(data)
@api_bp.route("/export/aliases", methods=["GET"])
@require_api_auth
def export_aliases():
"""
Get user aliases as importable CSV file
Output:
Importable CSV file
"""
return alias_export_csv(g.user)

186
app/api/views/mailbox.py Normal file
View File

@ -0,0 +1,186 @@
from smtplib import SMTPRecipientsRefused
from flask import g
from flask import jsonify
from flask import request
from app import mailbox_utils
from app.api.base import api_bp, require_api_auth
from app.dashboard.views.mailbox_detail import verify_mailbox_change
from app.db import Session
from app.email_utils import (
mailbox_already_used,
email_can_be_used_as_mailbox,
)
from app.models import Mailbox
from app.utils import sanitize_email
def mailbox_to_dict(mailbox: Mailbox):
return {
"id": mailbox.id,
"email": mailbox.email,
"verified": mailbox.verified,
"default": mailbox.user.default_mailbox_id == mailbox.id,
"creation_timestamp": mailbox.created_at.timestamp,
"nb_alias": mailbox.nb_alias(),
}
@api_bp.route("/mailboxes", methods=["POST"])
@require_api_auth
def create_mailbox():
"""
Create a new mailbox. User needs to verify the mailbox via an activation email.
Input:
email: in body
Output:
the new mailbox dict
"""
user = g.user
mailbox_email = sanitize_email(request.get_json().get("email"))
try:
new_mailbox = mailbox_utils.create_mailbox(user, mailbox_email).mailbox
except mailbox_utils.MailboxError as e:
return jsonify(error=e.msg), 400
return (
jsonify(mailbox_to_dict(new_mailbox)),
201,
)
@api_bp.route("/mailboxes/<int:mailbox_id>", methods=["DELETE"])
@require_api_auth
def delete_mailbox(mailbox_id):
"""
Delete mailbox
Input:
mailbox_id: in url
(optional) transfer_aliases_to: in body. Id of the new mailbox for the aliases.
If omitted or the value is set to -1,
the aliases of the mailbox will be deleted too.
Output:
200 if deleted successfully
"""
user = g.user
data = request.get_json() or {}
transfer_mailbox_id = data.get("transfer_aliases_to")
if transfer_mailbox_id and int(transfer_mailbox_id) >= 0:
transfer_mailbox_id = int(transfer_mailbox_id)
else:
transfer_mailbox_id = None
try:
mailbox_utils.delete_mailbox(user, mailbox_id, transfer_mailbox_id)
except mailbox_utils.MailboxError as e:
return jsonify(error=e.msg), 400
return jsonify(deleted=True), 200
@api_bp.route("/mailboxes/<int:mailbox_id>", methods=["PUT"])
@require_api_auth
def update_mailbox(mailbox_id):
"""
Update mailbox
Input:
mailbox_id: in url
(optional) default: in body. Set a mailbox as the default mailbox.
(optional) email: in body. Change a mailbox email.
(optional) cancel_email_change: in body. Cancel mailbox email change.
Output:
200 if updated successfully
"""
user = g.user
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != user.id:
return jsonify(error="Forbidden"), 403
data = request.get_json() or {}
changed = False
if "default" in data:
is_default = data.get("default")
if is_default:
if not mailbox.verified:
return (
jsonify(
error="Unverified mailbox cannot be used as default mailbox"
),
400,
)
user.default_mailbox_id = mailbox.id
changed = True
if "email" in data:
new_email = sanitize_email(data.get("email"))
if mailbox_already_used(new_email, user):
return jsonify(error=f"{new_email} already used"), 400
elif not email_can_be_used_as_mailbox(new_email):
return (
jsonify(
error=f"{new_email} cannot be used. Please note a mailbox cannot "
f"be a disposable email address"
),
400,
)
try:
verify_mailbox_change(user, mailbox, new_email)
except SMTPRecipientsRefused:
return jsonify(error=f"Incorrect mailbox, please recheck {new_email}"), 400
else:
mailbox.new_email = new_email
changed = True
if "cancel_email_change" in data:
cancel_email_change = data.get("cancel_email_change")
if cancel_email_change:
mailbox.new_email = None
changed = True
if changed:
Session.commit()
return jsonify(updated=True), 200
@api_bp.route("/mailboxes", methods=["GET"])
@require_api_auth
def get_mailboxes():
"""
Get verified mailboxes
Output:
- mailboxes: list of mailbox dict
"""
user = g.user
return (
jsonify(mailboxes=[mailbox_to_dict(mb) for mb in user.mailboxes()]),
200,
)
@api_bp.route("/v2/mailboxes", methods=["GET"])
@require_api_auth
def get_mailboxes_v2():
"""
Get all mailboxes - including unverified mailboxes
Output:
- mailboxes: list of mailbox dict
"""
user = g.user
mailboxes = []
for mailbox in Mailbox.filter_by(user_id=user.id):
mailboxes.append(mailbox)
return (
jsonify(mailboxes=[mailbox_to_dict(mb) for mb in mailboxes]),
200,
)

View File

@ -1,25 +1,42 @@
from flask import g from flask import g
from flask import jsonify, request from flask import jsonify, request
from flask_cors import cross_origin
from app.api.base import api_bp, verify_api_key from app import parallel_limiter
from app.config import MAX_NB_EMAIL_FREE_PLAN from app.alias_suffix import check_suffix_signature, verify_prefix_suffix
from app.dashboard.views.custom_alias import verify_prefix_suffix from app.alias_utils import check_alias_prefix
from app.extensions import db from app.api.base import api_bp, require_api_auth
from app.api.serializer import (
serialize_alias_info_v2,
get_alias_info_v2,
)
from app.config import MAX_NB_EMAIL_FREE_PLAN, ALIAS_LIMIT
from app.db import Session
from app.extensions import limiter
from app.log import LOG from app.log import LOG
from app.models import GenEmail, AliasUsedOn, User from app.models import (
Alias,
AliasUsedOn,
User,
DeletedAlias,
DomainDeletedAlias,
Mailbox,
AliasMailbox,
)
from app.utils import convert_to_id from app.utils import convert_to_id
@api_bp.route("/alias/custom/new", methods=["POST"]) @api_bp.route("/v2/alias/custom/new", methods=["POST"])
@cross_origin() @limiter.limit(ALIAS_LIMIT)
@verify_api_key @require_api_auth
def new_custom_alias(): @parallel_limiter.lock(name="alias_creation")
def new_custom_alias_v2():
""" """
Create a new custom alias Create a new custom alias
Same as v1 but signed_suffix is actually the suffix with signature, e.g.
.random_word@SL.co.Xq19rQ.s99uWQ7jD1s5JZDZqczYI5TbNNU
Input: Input:
alias_prefix, for ex "www_groupon_com" alias_prefix, for ex "www_groupon_com"
alias_suffix, either .random_letters@simplelogin.co or @my-domain.com signed_suffix, either .random_letters@simplelogin.co or @my-domain.com
optional "hostname" in args optional "hostname" in args
optional "note" optional "note"
Output: Output:
@ -38,33 +55,181 @@ def new_custom_alias():
400, 400,
) )
user_custom_domains = [cd.domain for cd in user.verified_custom_domains()]
hostname = request.args.get("hostname") hostname = request.args.get("hostname")
data = request.get_json() data = request.get_json()
if not data: if not data:
return jsonify(error="request body cannot be empty"), 400 return jsonify(error="request body cannot be empty"), 400
alias_prefix = data.get("alias_prefix", "").strip() alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "")
alias_suffix = data.get("alias_suffix", "").strip() signed_suffix = data.get("signed_suffix", "").strip()
note = data.get("note") note = data.get("note")
alias_prefix = convert_to_id(alias_prefix) alias_prefix = convert_to_id(alias_prefix)
if not verify_prefix_suffix(user, alias_prefix, alias_suffix, user_custom_domains): try:
alias_suffix = check_suffix_signature(signed_suffix)
if not alias_suffix:
LOG.w("Alias creation time expired for %s", user)
return jsonify(error="Alias creation time is expired, please retry"), 412
except Exception:
LOG.w("Alias suffix is tampered, user %s", user)
return jsonify(error="Tampered suffix"), 400
if not verify_prefix_suffix(user, alias_prefix, alias_suffix):
return jsonify(error="wrong alias prefix or suffix"), 400 return jsonify(error="wrong alias prefix or suffix"), 400
full_alias = alias_prefix + alias_suffix full_alias = alias_prefix + alias_suffix
if GenEmail.get_by(email=full_alias): if (
Alias.get_by(email=full_alias)
or DeletedAlias.get_by(email=full_alias)
or DomainDeletedAlias.get_by(email=full_alias)
):
LOG.d("full alias already used %s", full_alias) LOG.d("full alias already used %s", full_alias)
return jsonify(error=f"alias {full_alias} already exists"), 409 return jsonify(error=f"alias {full_alias} already exists"), 409
gen_email = GenEmail.create( if ".." in full_alias:
user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note return (
jsonify(error="2 consecutive dot signs aren't allowed in an email address"),
400,
)
alias = Alias.create(
user_id=user.id,
email=full_alias,
mailbox_id=user.default_mailbox_id,
note=note,
) )
db.session.commit()
Session.commit()
if hostname: if hostname:
AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname) AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id)
db.session.commit() Session.commit()
return jsonify(alias=full_alias), 201 return (
jsonify(alias=full_alias, **serialize_alias_info_v2(get_alias_info_v2(alias))),
201,
)
@api_bp.route("/v3/alias/custom/new", methods=["POST"])
@limiter.limit(ALIAS_LIMIT)
@require_api_auth
@parallel_limiter.lock(name="alias_creation")
def new_custom_alias_v3():
"""
Create a new custom alias
Same as v2 but accept a list of mailboxes as input
Input:
alias_prefix, for ex "www_groupon_com"
signed_suffix, either .random_letters@simplelogin.co or @my-domain.com
mailbox_ids: list of int
optional "hostname" in args
optional "note"
optional "name"
Output:
201 if success
409 if the alias already exists
"""
user: User = g.user
if not user.can_create_new_alias():
LOG.d("user %s cannot create any custom alias", user)
return (
jsonify(
error="You have reached the limitation of a free account with the maximum of "
f"{MAX_NB_EMAIL_FREE_PLAN} aliases, please upgrade your plan to create more aliases"
),
400,
)
hostname = request.args.get("hostname")
data = request.get_json()
if not data:
return jsonify(error="request body cannot be empty"), 400
if not isinstance(data, dict):
return jsonify(error="request body does not follow the required format"), 400
alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "")
signed_suffix = data.get("signed_suffix", "") or ""
signed_suffix = signed_suffix.strip()
mailbox_ids = data.get("mailbox_ids")
note = data.get("note")
name = data.get("name")
if name:
name = name.replace("\n", "")
alias_prefix = convert_to_id(alias_prefix)
if not check_alias_prefix(alias_prefix):
return jsonify(error="alias prefix invalid format or too long"), 400
# check if mailbox is not tempered with
if not isinstance(mailbox_ids, list):
return jsonify(error="mailbox_ids must be an array of id"), 400
mailboxes = []
for mailbox_id in mailbox_ids:
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != user.id or not mailbox.verified:
return jsonify(error="Errors with Mailbox"), 400
mailboxes.append(mailbox)
if not mailboxes:
return jsonify(error="At least one mailbox must be selected"), 400
# hypothesis: user will click on the button in the 600 secs
try:
alias_suffix = check_suffix_signature(signed_suffix)
if not alias_suffix:
LOG.w("Alias creation time expired for %s", user)
return jsonify(error="Alias creation time is expired, please retry"), 412
except Exception:
LOG.w("Alias suffix is tampered, user %s", user)
return jsonify(error="Tampered suffix"), 400
if not verify_prefix_suffix(user, alias_prefix, alias_suffix):
return jsonify(error="wrong alias prefix or suffix"), 400
full_alias = alias_prefix + alias_suffix
if (
Alias.get_by(email=full_alias)
or DeletedAlias.get_by(email=full_alias)
or DomainDeletedAlias.get_by(email=full_alias)
):
LOG.d("full alias already used %s", full_alias)
return jsonify(error=f"alias {full_alias} already exists"), 409
if ".." in full_alias:
return (
jsonify(error="2 consecutive dot signs aren't allowed in an email address"),
400,
)
alias = Alias.create(
user_id=user.id,
email=full_alias,
note=note,
name=name or None,
mailbox_id=mailboxes[0].id,
)
Session.flush()
for i in range(1, len(mailboxes)):
AliasMailbox.create(
alias_id=alias.id,
mailbox_id=mailboxes[i].id,
)
Session.commit()
if hostname:
AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id)
Session.commit()
return (
jsonify(alias=full_alias, **serialize_alias_info_v2(get_alias_info_v2(alias))),
201,
)

View File

@ -1,17 +1,27 @@
import tldextract
from flask import g from flask import g
from flask import jsonify, request from flask import jsonify, request
from flask_cors import cross_origin
from app.api.base import api_bp, verify_api_key from app import parallel_limiter
from app.config import MAX_NB_EMAIL_FREE_PLAN from app.alias_suffix import get_alias_suffixes
from app.extensions import db from app.api.base import api_bp, require_api_auth
from app.api.serializer import (
get_alias_info_v2,
serialize_alias_info_v2,
)
from app.config import MAX_NB_EMAIL_FREE_PLAN, ALIAS_LIMIT
from app.db import Session
from app.errors import AliasInTrashError
from app.extensions import limiter
from app.log import LOG from app.log import LOG
from app.models import GenEmail, AliasUsedOn, AliasGeneratorEnum from app.models import Alias, AliasUsedOn, AliasGeneratorEnum
from app.utils import convert_to_id
@api_bp.route("/alias/random/new", methods=["POST"]) @api_bp.route("/alias/random/new", methods=["POST"])
@cross_origin() @limiter.limit(ALIAS_LIMIT)
@verify_api_key @require_api_auth
@parallel_limiter.lock(name="alias_creation")
def new_random_alias(): def new_random_alias():
""" """
Create a new random alias Create a new random alias
@ -37,22 +47,71 @@ def new_random_alias():
if data: if data:
note = data.get("note") note = data.get("note")
scheme = user.alias_generator alias = None
mode = request.args.get("mode")
if mode:
if mode == "word":
scheme = AliasGeneratorEnum.word.value
elif mode == "uuid":
scheme = AliasGeneratorEnum.uuid.value
else:
return jsonify(error=f"{mode} must be either word or alias"), 400
gen_email = GenEmail.create_new_random(user=user, scheme=scheme, note=note)
db.session.commit()
# custom alias suggestion and suffix
hostname = request.args.get("hostname") hostname = request.args.get("hostname")
if hostname: if hostname and user.include_website_in_one_click_alias:
AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname) LOG.d("Use %s to create new alias", hostname)
db.session.commit() # keep only the domain name of hostname, ignore TLD and subdomain
# for ex www.groupon.com -> groupon
ext = tldextract.extract(hostname)
prefix_suggestion = ext.domain
prefix_suggestion = convert_to_id(prefix_suggestion)
return jsonify(alias=gen_email.email), 201 suffixes = get_alias_suffixes(user)
# use the first suffix
suggested_alias = prefix_suggestion + suffixes[0].suffix
alias = Alias.get_by(email=suggested_alias)
# cannot use this alias as it belongs to another user
if alias and not alias.user_id == user.id:
LOG.d("%s belongs to another user", alias)
alias = None
elif alias and alias.user_id == user.id:
# make sure alias was created for this website
if AliasUsedOn.get_by(
alias_id=alias.id, hostname=hostname, user_id=alias.user_id
):
LOG.d("Use existing alias %s", alias)
else:
LOG.d("%s wasn't created for this website %s", alias, hostname)
alias = None
elif not alias:
LOG.d("create new alias %s", suggested_alias)
try:
alias = Alias.create(
user_id=user.id,
email=suggested_alias,
note=note,
mailbox_id=user.default_mailbox_id,
commit=True,
)
except AliasInTrashError:
LOG.i("Alias %s is in trash", suggested_alias)
alias = None
if not alias:
scheme = user.alias_generator
mode = request.args.get("mode")
if mode:
if mode == "word":
scheme = AliasGeneratorEnum.word.value
elif mode == "uuid":
scheme = AliasGeneratorEnum.uuid.value
else:
return jsonify(error=f"{mode} must be either word or uuid"), 400
alias = Alias.create_new_random(user=user, scheme=scheme, note=note)
Session.commit()
if hostname and not AliasUsedOn.get_by(alias_id=alias.id, hostname=hostname):
AliasUsedOn.create(
alias_id=alias.id, hostname=hostname, user_id=alias.user_id, commit=True
)
return (
jsonify(alias=alias.email, **serialize_alias_info_v2(get_alias_info_v2(alias))),
201,
)

View File

@ -0,0 +1,83 @@
from flask import g
from flask import jsonify
from flask import request
from app.api.base import api_bp, require_api_auth
from app.config import PAGE_LIMIT
from app.db import Session
from app.models import Notification
@api_bp.route("/notifications", methods=["GET"])
@require_api_auth
def get_notifications():
"""
Get notifications
Input:
- page: in url. Starts at 0
Output:
- more: boolean. Whether there's more notification to load
- notifications: list of notifications.
- id
- message
- title
- read
- created_at
"""
user = g.user
try:
page = int(request.args.get("page"))
except (ValueError, TypeError):
return jsonify(error="page must be provided in request query"), 400
notifications = (
Notification.filter_by(user_id=user.id)
.order_by(Notification.read, Notification.created_at.desc())
.limit(PAGE_LIMIT + 1) # load a record more to know whether there's more
.offset(page * PAGE_LIMIT)
.all()
)
have_more = len(notifications) > PAGE_LIMIT
return (
jsonify(
more=have_more,
notifications=[
{
"id": notification.id,
"message": notification.message,
"title": notification.title,
"read": notification.read,
"created_at": notification.created_at.humanize(),
}
for notification in notifications[:PAGE_LIMIT]
],
),
200,
)
@api_bp.route("/notifications/<int:notification_id>/read", methods=["POST"])
@require_api_auth
def mark_as_read(notification_id):
"""
Mark a notification as read
Input:
notification_id: in url
Output:
200 if updated successfully
"""
user = g.user
notification = Notification.get(notification_id)
if not notification or notification.user_id != user.id:
return jsonify(error="Forbidden"), 403
notification.read = True
Session.commit()
return jsonify(done=True), 200

51
app/api/views/phone.py Normal file
View File

@ -0,0 +1,51 @@
import arrow
from flask import g
from flask import jsonify
from app.api.base import api_bp, require_api_auth
from app.models import (
PhoneReservation,
PhoneMessage,
)
@api_bp.route("/phone/reservations/<int:reservation_id>", methods=["GET", "POST"])
@require_api_auth
def phone_messages(reservation_id):
"""
Return messages during this reservation
Output:
- messages: list of alias:
- id
- from_number
- body
- created_at: e.g. 5 minutes ago
"""
user = g.user
reservation: PhoneReservation = PhoneReservation.get(reservation_id)
if not reservation or reservation.user_id != user.id:
return jsonify(error="Invalid reservation"), 400
phone_number = reservation.number
messages = PhoneMessage.filter(
PhoneMessage.number_id == phone_number.id,
PhoneMessage.created_at > reservation.start,
PhoneMessage.created_at < reservation.end,
).all()
return (
jsonify(
messages=[
{
"id": message.id,
"from_number": message.from_number,
"body": message.body,
"created_at": message.created_at.humanize(),
}
for message in messages
],
ended=reservation.end < arrow.now(),
),
200,
)

148
app/api/views/setting.py Normal file
View File

@ -0,0 +1,148 @@
import arrow
from flask import jsonify, g, request
from app.api.base import api_bp, require_api_auth
from app.db import Session
from app.log import LOG
from app.models import (
User,
AliasGeneratorEnum,
SLDomain,
CustomDomain,
SenderFormatEnum,
AliasSuffixEnum,
)
from app.proton.utils import perform_proton_account_unlink
def setting_to_dict(user: User):
ret = {
"notification": user.notification,
"alias_generator": "word"
if user.alias_generator == AliasGeneratorEnum.word.value
else "uuid",
"random_alias_default_domain": user.default_random_alias_domain(),
# return the default sender format (AT) in case user uses a non-supported sender format
"sender_format": SenderFormatEnum.get_name(user.sender_format)
or SenderFormatEnum.AT.name,
"random_alias_suffix": AliasSuffixEnum.get_name(user.random_alias_suffix),
}
return ret
@api_bp.route("/setting")
@require_api_auth
def get_setting():
"""
Return user setting
"""
user = g.user
return jsonify(setting_to_dict(user))
@api_bp.route("/setting", methods=["PATCH"])
@require_api_auth
def update_setting():
"""
Update user setting
Input:
- notification: bool
- alias_generator: word|uuid
- random_alias_default_domain: str
"""
user = g.user
data = request.get_json() or {}
if "notification" in data:
user.notification = data["notification"]
if "alias_generator" in data:
alias_generator = data["alias_generator"]
if alias_generator not in ["word", "uuid"]:
return jsonify(error="Invalid alias_generator"), 400
if alias_generator == "word":
user.alias_generator = AliasGeneratorEnum.word.value
else:
user.alias_generator = AliasGeneratorEnum.uuid.value
if "sender_format" in data:
sender_format = data["sender_format"]
if not SenderFormatEnum.has_name(sender_format):
return jsonify(error="Invalid sender_format"), 400
user.sender_format = SenderFormatEnum.get_value(sender_format)
user.sender_format_updated_at = arrow.now()
if "random_alias_suffix" in data:
random_alias_suffix = data["random_alias_suffix"]
if not AliasSuffixEnum.has_name(random_alias_suffix):
return jsonify(error="Invalid random_alias_suffix"), 400
user.random_alias_suffix = AliasSuffixEnum.get_value(random_alias_suffix)
if "random_alias_default_domain" in data:
default_domain = data["random_alias_default_domain"]
sl_domain: SLDomain = SLDomain.get_by(domain=default_domain)
if sl_domain:
if sl_domain.premium_only and not user.is_premium():
return jsonify(error="You cannot use this domain"), 400
user.default_alias_public_domain_id = sl_domain.id
user.default_alias_custom_domain_id = None
else:
custom_domain = CustomDomain.get_by(domain=default_domain)
if not custom_domain:
return jsonify(error="invalid domain"), 400
# sanity check
if custom_domain.user_id != user.id or not custom_domain.verified:
LOG.w("%s cannot use domain %s", user, default_domain)
return jsonify(error="invalid domain"), 400
else:
user.default_alias_custom_domain_id = custom_domain.id
user.default_alias_public_domain_id = None
Session.commit()
return jsonify(setting_to_dict(user))
@api_bp.route("/setting/domains")
@require_api_auth
def get_available_domains_for_random_alias():
"""
Available domains for random alias
"""
user = g.user
ret = [
(is_sl, domain) for is_sl, domain in user.available_domains_for_random_alias()
]
return jsonify(ret)
@api_bp.route("/v2/setting/domains")
@require_api_auth
def get_available_domains_for_random_alias_v2():
"""
Available domains for random alias
"""
user = g.user
ret = [
{"domain": domain, "is_custom": not is_sl}
for is_sl, domain in user.available_domains_for_random_alias()
]
return jsonify(ret)
@api_bp.route("/setting/unlink_proton_account", methods=["DELETE"])
@require_api_auth
def unlink_proton_account():
user = g.user
perform_proton_account_unlink(user)
return jsonify({"ok": True})

27
app/api/views/sudo.py Normal file
View File

@ -0,0 +1,27 @@
from flask import jsonify, g, request
from sqlalchemy_utils.types.arrow import arrow
from app.api.base import api_bp, require_api_auth
from app.db import Session
@api_bp.route("/sudo", methods=["PATCH"])
@require_api_auth
def enter_sudo():
"""
Enter sudo mode
Input
- password: user password to validate request to enter sudo mode
"""
user = g.user
data = request.get_json() or {}
if "password" not in data:
return jsonify(error="Invalid password"), 403
if not user.check_password(data["password"]):
return jsonify(error="Invalid password"), 403
g.api_key.sudo_mode_at = arrow.now()
Session.commit()
return jsonify(ok=True)

46
app/api/views/user.py Normal file
View File

@ -0,0 +1,46 @@
from flask import jsonify, g
from sqlalchemy_utils.types.arrow import arrow
from app.api.base import api_bp, require_api_sudo, require_api_auth
from app import config
from app.extensions import limiter
from app.log import LOG
from app.models import Job, ApiToCookieToken
@api_bp.route("/user", methods=["DELETE"])
@require_api_sudo
def delete_user():
"""
Delete the user. Requires sudo mode.
"""
# Schedule delete account job
LOG.w("schedule delete account job for %s", g.user)
Job.create(
name=config.JOB_DELETE_ACCOUNT,
payload={"user_id": g.user.id},
run_at=arrow.now(),
commit=True,
)
return jsonify(ok=True)
@api_bp.route("/user/cookie_token", methods=["GET"])
@require_api_auth
@limiter.limit("5/minute")
def get_api_session_token():
"""
Get a temporary token to exchange it for a cookie based session
Output:
200 and a temporary random token
{
token: "asdli3ldq39h9hd3",
}
"""
token = ApiToCookieToken.create(
user=g.user,
api_key_id=g.api_key.id,
commit=True,
)
return jsonify({"token": token.code})

View File

@ -1,22 +1,163 @@
from flask import jsonify, request, g import base64
from flask_cors import cross_origin import dataclasses
from sqlalchemy import desc from io import BytesIO
from typing import Optional
from app.api.base import api_bp, verify_api_key from flask import jsonify, g, request, make_response
from app.config import EMAIL_DOMAIN
from app.extensions import db from app import s3, config
from app.log import LOG from app.api.base import api_bp, require_api_auth
from app.models import AliasUsedOn, GenEmail, User from app.config import SESSION_COOKIE_NAME
from app.utils import convert_to_id, random_word from app.dashboard.views.index import get_stats
from app.db import Session
from app.image_validation import detect_image_format, ImageFormat
from app.models import ApiKey, File, PartnerUser, User
from app.proton.utils import get_proton_partner
from app.session import logout_session
from app.utils import random_string
def get_connected_proton_address(user: User) -> Optional[str]:
proton_partner = get_proton_partner()
partner_user = PartnerUser.get_by(user_id=user.id, partner_id=proton_partner.id)
if partner_user is None:
return None
return partner_user.partner_email
def user_to_dict(user: User) -> dict:
ret = {
"name": user.name or "",
"is_premium": user.is_premium(),
"email": user.email,
"in_trial": user.in_trial(),
"max_alias_free_plan": user.max_alias_for_free_account(),
"connected_proton_address": None,
"can_create_reverse_alias": user.can_create_contacts(),
}
if config.CONNECT_WITH_PROTON:
ret["connected_proton_address"] = get_connected_proton_address(user)
if user.profile_picture_id:
ret["profile_picture_url"] = user.profile_picture.get_url()
else:
ret["profile_picture_url"] = None
return ret
@api_bp.route("/user_info") @api_bp.route("/user_info")
@cross_origin() @require_api_auth
@verify_api_key
def user_info(): def user_info():
""" """
Return user info given the api-key Return user info given the api-key
Output as json
- name
- is_premium
- email
- in_trial
- max_alias_free
- is_connected_with_proton
- can_create_reverse_alias
""" """
user = g.user user = g.user
return jsonify({"name": user.name, "is_premium": user.is_premium()}) return jsonify(user_to_dict(user))
@api_bp.route("/user_info", methods=["PATCH"])
@require_api_auth
def update_user_info():
"""
Input
- profile_picture (optional): base64 of the profile picture. Set to null to remove the profile picture
- name (optional)
"""
user = g.user
data = request.get_json() or {}
if "profile_picture" in data:
if user.profile_picture_id:
file = user.profile_picture
user.profile_picture_id = None
Session.flush()
if file:
File.delete(file.id)
s3.delete(file.path)
Session.flush()
else:
raw_data = base64.decodebytes(data["profile_picture"].encode())
if detect_image_format(raw_data) == ImageFormat.Unknown:
return jsonify(error="Unsupported image format"), 400
file_path = random_string(30)
file = File.create(user_id=user.id, path=file_path)
Session.flush()
s3.upload_from_bytesio(file_path, BytesIO(raw_data))
user.profile_picture_id = file.id
Session.flush()
if "name" in data:
user.name = data["name"]
Session.commit()
return jsonify(user_to_dict(user))
@api_bp.route("/api_key", methods=["POST"])
@require_api_auth
def create_api_key():
"""Used to create a new api key
Input:
- device
Output:
- api_key
"""
data = request.get_json()
if not data:
return jsonify(error="request body cannot be empty"), 400
device = data.get("device")
api_key = ApiKey.create(user_id=g.user.id, name=device)
Session.commit()
return jsonify(api_key=api_key.code), 201
@api_bp.route("/logout", methods=["GET"])
@require_api_auth
def logout():
"""
Log user out on the web, i.e. remove the cookie
Output:
- 200
"""
logout_session()
response = make_response(jsonify(msg="User is logged out"), 200)
response.delete_cookie(SESSION_COOKIE_NAME)
return response
@api_bp.route("/stats")
@require_api_auth
def user_stats():
"""
Return stats
Output as json
- nb_alias
- nb_forward
- nb_reply
- nb_block
"""
user = g.user
stats = get_stats(user)
return jsonify(dataclasses.asdict(stats))

View File

@ -9,6 +9,33 @@ from .views import (
github, github,
google, google,
facebook, facebook,
proton,
change_email, change_email,
mfa, mfa,
fido,
social,
recovery,
api_to_cookie,
oidc,
) )
__all__ = [
"login",
"logout",
"register",
"activate",
"resend_activation",
"reset_password",
"forgot_password",
"github",
"google",
"facebook",
"proton",
"change_email",
"mfa",
"fido",
"social",
"recovery",
"api_to_cookie",
"oidc",
]

View File

@ -1,30 +0,0 @@
{% extends "single.html" %}
{% block title %}
Change Email
{% endblock %}
{% block single_content %}
{% if error %}
<div class="text-danger text-center mb-4">{{ error }}</div>
{% endif %}
{% if incorrect_code %}
<div class="text-danger text-center h4">
The link is incorrect. <br><br>
Please go to <a href="{{ url_for('dashboard.setting') }}"
class="btn btn-warning">settings</a>
page to re-send confirmation email.
</div>
{% endif %}
{% if expired_code %}
<div class="text-danger text-center h4">
The link is already expired. <br><br>
Please go to <a href="{{ url_for('dashboard.setting') }}"
class="btn btn-warning">settings</a>
page to re-send confirmation email.
</div>
{% endif %}
{% endblock %}

View File

@ -1,102 +0,0 @@
{% extends "single.html" %}
{% block title %}
Login
{% endblock %}
{% block head %}
<style>
.col-login {
max-width: 48rem;
}
</style>
{% endblock %}
{% block single_content %}
<h1 class="h2 text-center">Welcome back!</h1>
<div class="row">
<div class="col-md-6">
{% if show_resend_activation %}
<div class="text-center text-muted small mb-4">
You haven't received the activation email?
<a href="{{ url_for('auth.resend_activation') }}">Resend</a>
</div>
{% endif %}
<div class="card">
<form method="post">
{{ form.csrf_token }}
<div class="card-body p-6">
<div class="form-group">
<label class="form-label">Email address</label>
{{ form.email(class="form-control", type="email") }}
{{ render_field_errors(form.email) }}
</div>
<div class="form-group">
<label class="form-label">
Password
</label>
{{ form.password(class="form-control", type="password") }}
{{ render_field_errors(form.password) }}
<div class="text-muted">
<a href="{{ url_for('auth.forgot_password') }}" class="small">
I forgot my password
</a>
</div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary btn-block">Log in</button>
</div>
<div class="text-center text-muted mt-2">
Don't have an account yet? <a href="{{ url_for('auth.register') }}">Sign up</a>
</div>
</div>
</form>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body p-6">
<div class="card-title text-center">Or with social login
</div>
<a href="{{ url_for('auth.github_login', next=next_url) }}"
class="btn btn-block btn-social btn-github">
<i class="fa fa-github"></i> Sign in with Github
</a>
<a href="{{ url_for('auth.google_login', next=next_url) }}"
class="btn btn-block btn-social btn-google">
<i class="fa fa-google"></i> Sign in with Google
</a>
<a href="{{ url_for('auth.facebook_login', next=next_url) }}"
class="btn btn-block btn-social btn-facebook">
<i class="fa fa-facebook"></i> Sign in with Facebook
</a>
</div>
<div class="text-center p-3" style="font-size: 12px; font-weight: 300; margin: auto">
We do not use the Facebook/Google SDK to avoid their trackers. <br>
However when using a social login button, please keep in mind that this social network will <b>know</b> that
you are using SimpleLogin.
<span class="badge badge-warning">Warning</span>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,14 +0,0 @@
{% extends "single.html" %}
{% block title %}
Logout
{% endblock %}
{% block single_content %}
<div class="text-center text-muted">
You are logged out.
<a href="{{ url_for('auth.login') }}">Login</a>
</div>
{% endblock %}

View File

@ -1,33 +0,0 @@
{% extends "single.html" %}
{% block title %}
MFA
{% endblock %}
{% block single_content %}
<div class="bg-white p-6" style="margin: auto">
<div>
Your account is protected with multi-factor authentication (MFA). <br>
To continue with the sign-in you need to provide the access code from your authenticator.
</div>
<form method="post">
{{ otp_token_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<div class="font-weight-bold mt-5">Token</div>
<div class="small-text">Please enter the 6-digit number displayed in your MFA application (Google Authenticator,
Authy) here
</div>
{{ otp_token_form.token(class="form-control", autofocus="true") }}
{{ render_field_errors(otp_token_form.token) }}
<button class="btn btn-success mt-2">Validate</button>
</form>
</div>
{% endblock %}

View File

@ -1,96 +0,0 @@
{% extends "single.html" %}
{% block title %}
Register
{% endblock %}
{% block head %}
<style>
.col-login {
max-width: 48rem;
}
</style>
{% endblock %}
{% block single_content %}
<h1 class="h3 text-center">Create your SimpleLogin account now</h1>
<div class="row">
<div class="col-md-6">
<div class="card">
<form method="post">
{{ form.csrf_token }}
<div class="card-body p-6">
<div class="form-group">
<label class="form-label">Email address</label>
{{ form.email(class="form-control", type="email") }}
{{ render_field_errors(form.email) }}
</div>
<div class="form-group">
<label class="form-label">Password</label>
{{ form.password(class="form-control", type="password") }}
{{ render_field_errors(form.password) }}
</div>
<!-- TODO: add terms
<div class="form-group">
<label class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input"/>
<span class="custom-control-label">Agree the <a href="terms.html">terms and policy</a></span>
</label>
</div>
-->
<div class="form-footer">
<button type="submit" class="btn btn-primary btn-block">Create your SimpleLogin account</button>
</div>
</div>
</form>
<div class="text-center text-muted mb-6">
Already have account? <a href="{{ url_for('auth.login') }}">Sign in</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body p-6">
<div class="card-title text-center">Or with social login</div>
<a href="{{ url_for('auth.github_login', next=next_url) }}"
class="btn btn-block btn-social btn-github">
<i class="fa fa-github"></i> Sign up with Github
</a>
<a href="{{ url_for('auth.google_login', next=next_url) }}"
class="btn btn-block btn-social btn-google">
<i class="fa fa-google"></i> Sign up with Google
</a>
<a href="{{ url_for('auth.facebook_login', next=next_url) }}"
class="btn btn-block btn-social btn-facebook">
<i class="fa fa-facebook"></i> Sign up with Facebook
</a>
</div>
<div class="text-center p-3" style="font-size: 12px; font-weight: 300; margin: auto">
We do not use the Facebook/Google SDK to avoid their trackers. <br>
However when using a social login button, please keep in mind that this social network will <b>know</b> that
you are using SimpleLogin.
<span class="badge badge-warning">Warning</span>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,22 +0,0 @@
{% extends "single.html" %}
{% block title %}
Activation Email Sent
{% endblock %}
{% block single_content %}
<div class="text-center bg-white p-5" style="max-width: 50rem">
<h1>
An email to validate your email is on its way.
</h1>
<h3>
Please check your inbox/spam folder.
</h3>
<small>
Yeah we know. An email to confirm an email ...
</small>
</h1>
</div>
{% endblock %}

View File

@ -1,14 +1,19 @@
from flask import request, redirect, url_for, flash, render_template from flask import request, redirect, url_for, flash, render_template, g
from flask_login import login_user, current_user from flask_login import login_user, current_user
from app import email_utils from app import email_utils
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.extensions import db from app.db import Session
from app.extensions import limiter
from app.log import LOG from app.log import LOG
from app.models import ActivationCode from app.models import ActivationCode
from app.utils import sanitize_next_url
@auth_bp.route("/activate", methods=["GET", "POST"]) @auth_bp.route("/activate", methods=["GET", "POST"])
@limiter.limit(
"10/minute", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
)
def activate(): def activate():
if current_user.is_authenticated: if current_user.is_authenticated:
return ( return (
@ -21,6 +26,8 @@ def activate():
activation_code: ActivationCode = ActivationCode.get_by(code=code) activation_code: ActivationCode = ActivationCode.get_by(code=code)
if not activation_code: if not activation_code:
# Trigger rate limiter
g.deduct_limit = True
return ( return (
render_template( render_template(
"auth/activate.html", error="Activation code cannot be found" "auth/activate.html", error="Activation code cannot be found"
@ -41,19 +48,22 @@ def activate():
user = activation_code.user user = activation_code.user
user.activated = True user.activated = True
login_user(user) login_user(user)
email_utils.send_welcome_email(user)
# activation code is to be used only once # activation code is to be used only once
ActivationCode.delete(activation_code.id) ActivationCode.delete(activation_code.id)
db.session.commit() Session.commit()
flash("Your account has been activated", "success") flash("Your account has been activated", "success")
email_utils.send_welcome_email(user)
# The activation link contains the original page, for ex authorize page # The activation link contains the original page, for ex authorize page
if "next" in request.args: if "next" in request.args:
next_url = request.args.get("next") next_url = sanitize_next_url(request.args.get("next"))
LOG.debug("redirect user to %s", next_url) LOG.d("redirect user to %s", next_url)
return redirect(next_url) return redirect(next_url)
else: else:
LOG.debug("redirect user to dashboard") LOG.d("redirect user to dashboard")
return redirect(url_for("dashboard.index")) return redirect(url_for("dashboard.index"))
# todo: redirect to account_activated page when more features are added into the browser extension
# return redirect(url_for("onboarding.account_activated"))

View File

@ -0,0 +1,30 @@
import arrow
from flask import redirect, url_for, request, flash
from flask_login import login_user
from app.auth.base import auth_bp
from app.models import ApiToCookieToken
from app.utils import sanitize_next_url
@auth_bp.route("/api_to_cookie", methods=["GET"])
def api_to_cookie():
code = request.args.get("token")
if not code:
flash("Missing token", "error")
return redirect(url_for("auth.login"))
token = ApiToCookieToken.get_by(code=code)
if not token or token.created_at < arrow.now().shift(minutes=-5):
flash("Missing token", "error")
return redirect(url_for("auth.login"))
user = token.user
ApiToCookieToken.delete(token.id, commit=True)
login_user(user)
next_url = sanitize_next_url(request.args.get("next"))
if next_url:
return redirect(next_url)
else:
return redirect(url_for("dashboard.index"))

View File

@ -2,28 +2,37 @@ from flask import request, flash, render_template, redirect, url_for
from flask_login import login_user from flask_login import login_user
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.extensions import db from app.db import Session
from app.models import EmailChange from app.extensions import limiter
from app.log import LOG
from app.models import EmailChange, ResetPasswordCode
@auth_bp.route("/change_email", methods=["GET", "POST"]) @auth_bp.route("/change_email", methods=["GET", "POST"])
@limiter.limit("3/hour")
def change_email(): def change_email():
code = request.args.get("code") code = request.args.get("code")
email_change: EmailChange = EmailChange.get_by(code=code) email_change: EmailChange = EmailChange.get_by(code=code)
if not email_change: if not email_change:
return render_template("auth/change_email.html", incorrect_code=True) return render_template("auth/change_email.html")
if email_change.is_expired(): if email_change.is_expired():
return render_template("auth/change_email.html", expired_code=True) # delete the expired email
EmailChange.delete(email_change.id)
Session.commit()
return render_template("auth/change_email.html")
user = email_change.user user = email_change.user
old_email = user.email
user.email = email_change.new_email user.email = email_change.new_email
EmailChange.delete(email_change.id) EmailChange.delete(email_change.id)
db.session.commit() ResetPasswordCode.filter_by(user_id=user.id).delete()
Session.commit()
LOG.i(f"User {user} has changed their email from {old_email} to {user.email}")
flash("Your new email has been updated", "success") flash("Your new email has been updated", "success")
login_user(user) login_user(user)

View File

@ -1,22 +1,19 @@
from flask import request, session, redirect, url_for, flash from flask import request, session, redirect, url_for, flash
from flask_login import login_user
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from requests_oauthlib.compliance_fixes import facebook_compliance_fix from requests_oauthlib.compliance_fixes import facebook_compliance_fix
from app import email_utils
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.auth.views.google import create_file_from_url from app.auth.views.google import create_file_from_url
from app.config import ( from app.config import (
URL, URL,
FACEBOOK_CLIENT_ID, FACEBOOK_CLIENT_ID,
FACEBOOK_CLIENT_SECRET, FACEBOOK_CLIENT_SECRET,
DISABLE_REGISTRATION,
) )
from app.extensions import db from app.db import Session
from app.log import LOG from app.log import LOG
from app.models import User, SocialAuth from app.models import User, SocialAuth
from .login_utils import after_login from .login_utils import after_login
from ...email_utils import can_be_used_as_personal_email, email_already_used from ...utils import sanitize_email, sanitize_next_url
_authorization_base_url = "https://www.facebook.com/dialog/oauth" _authorization_base_url = "https://www.facebook.com/dialog/oauth"
_token_url = "https://graph.facebook.com/oauth/access_token" _token_url = "https://graph.facebook.com/oauth/access_token"
@ -33,7 +30,7 @@ def facebook_login():
# to avoid flask-login displaying the login error message # to avoid flask-login displaying the login error message
session.pop("_flashes", None) session.pop("_flashes", None)
next_url = request.args.get("next") next_url = sanitize_next_url(request.args.get("next"))
# Facebook does not allow to append param to redirect_uri # Facebook does not allow to append param to redirect_uri
# we need to pass the next url by session # we need to pass the next url by session
@ -65,7 +62,7 @@ def facebook_callback():
redirect_uri=_redirect_uri, redirect_uri=_redirect_uri,
) )
facebook = facebook_compliance_fix(facebook) facebook = facebook_compliance_fix(facebook)
token = facebook.fetch_token( facebook.fetch_token(
_token_url, _token_url,
client_secret=FACEBOOK_CLIENT_SECRET, client_secret=FACEBOOK_CLIENT_SECRET,
authorization_response=request.url, authorization_response=request.url,
@ -95,6 +92,7 @@ def facebook_callback():
) )
return redirect(url_for("auth.register")) return redirect(url_for("auth.register"))
email = sanitize_email(email)
user = User.get_by(email=email) user = User.get_by(email=email)
picture_url = facebook_user_data.get("picture", {}).get("data", {}).get("url") picture_url = facebook_user_data.get("picture", {}).get("data", {}).get("url")
@ -102,47 +100,28 @@ def facebook_callback():
if user: if user:
if picture_url and not user.profile_picture_id: if picture_url and not user.profile_picture_id:
LOG.d("set user profile picture to %s", picture_url) LOG.d("set user profile picture to %s", picture_url)
file = create_file_from_url(picture_url) file = create_file_from_url(user, picture_url)
user.profile_picture_id = file.id user.profile_picture_id = file.id
db.session.commit() Session.commit()
# create user
else: else:
if DISABLE_REGISTRATION: flash(
flash("Registration is closed", "error") "Sorry you cannot sign up via Facebook, please use email/password sign-up instead",
return redirect(url_for("auth.login")) "error",
if not can_be_used_as_personal_email(email) or email_already_used(email):
flash(f"You cannot use {email} as your personal inbox.", "error")
return redirect(url_for("auth.login"))
LOG.d("create facebook user with %s", facebook_user_data)
user = User.create(
email=email.lower(), name=facebook_user_data["name"], activated=True
) )
return redirect(url_for("auth.register"))
if picture_url:
LOG.d("set user profile picture to %s", picture_url)
file = create_file_from_url(picture_url)
user.profile_picture_id = file.id
db.session.commit()
login_user(user)
email_utils.send_welcome_email(user)
flash(f"Welcome to SimpleLogin {user.name}!", "success")
next_url = None next_url = None
# The activation link contains the original page, for ex authorize page # The activation link contains the original page, for ex authorize page
if "facebook_next_url" in session: if "facebook_next_url" in session:
next_url = session["facebook_next_url"] next_url = session["facebook_next_url"]
LOG.debug("redirect user to %s", next_url) LOG.d("redirect user to %s", next_url)
# reset the next_url to avoid user getting redirected at each login :) # reset the next_url to avoid user getting redirected at each login :)
session.pop("facebook_next_url", None) session.pop("facebook_next_url", None)
if not SocialAuth.get_by(user_id=user.id, social="facebook"): if not SocialAuth.get_by(user_id=user.id, social="facebook"):
SocialAuth.create(user_id=user.id, social="facebook") SocialAuth.create(user_id=user.id, social="facebook")
db.session.commit() Session.commit()
return after_login(user, next_url) return after_login(user, next_url)

173
app/auth/views/fido.py Normal file
View File

@ -0,0 +1,173 @@
import json
import secrets
from time import time
import webauthn
from flask import (
request,
render_template,
redirect,
url_for,
flash,
session,
make_response,
g,
)
from flask_login import login_user
from flask_wtf import FlaskForm
from wtforms import HiddenField, validators, BooleanField
from app.auth.base import auth_bp
from app.config import MFA_USER_ID
from app.config import RP_ID, URL
from app.db import Session
from app.extensions import limiter
from app.log import LOG
from app.models import User, Fido, MfaBrowser
from app.utils import sanitize_next_url
class FidoTokenForm(FlaskForm):
sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
remember = BooleanField(
"attr", default=False, description="Remember this browser for 30 days"
)
@auth_bp.route("/fido", methods=["GET", "POST"])
@limiter.limit(
"10/minute", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
)
def fido():
# passed from login page
user_id = session.get(MFA_USER_ID)
# user access this page directly without passing by login page
if not user_id:
flash("Unknown error, redirect back to main page", "warning")
return redirect(url_for("auth.login"))
user = User.get(user_id)
if not (user and user.fido_enabled()):
flash("Only user with security key linked should go to this page", "warning")
return redirect(url_for("auth.login"))
auto_activate = True
fido_token_form = FidoTokenForm()
next_url = sanitize_next_url(request.args.get("next"))
if request.cookies.get("mfa"):
browser = MfaBrowser.get_by(token=request.cookies.get("mfa"))
if browser and not browser.is_expired() and browser.user_id == user.id:
login_user(user)
flash("Welcome back!", "success")
# Redirect user to correct page
return redirect(next_url or url_for("dashboard.index"))
else:
# Trigger rate limiter
g.deduct_limit = True
# Handling POST requests
if fido_token_form.validate_on_submit():
try:
sk_assertion = json.loads(fido_token_form.sk_assertion.data)
except Exception:
flash("Key verification failed. Error: Invalid Payload", "warning")
return redirect(url_for("auth.login"))
challenge = session["fido_challenge"]
try:
fido_key = Fido.get_by(
uuid=user.fido_uuid, credential_id=sk_assertion["id"]
)
webauthn_user = webauthn.WebAuthnUser(
user.fido_uuid,
user.email,
user.name if user.name else user.email,
False,
fido_key.credential_id,
fido_key.public_key,
fido_key.sign_count,
RP_ID,
)
webauthn_assertion_response = webauthn.WebAuthnAssertionResponse(
webauthn_user, sk_assertion, challenge, URL, uv_required=False
)
new_sign_count = webauthn_assertion_response.verify()
except Exception as e:
LOG.w(f"An error occurred in WebAuthn verification process: {e}")
flash("Key verification failed.", "warning")
# Trigger rate limiter
g.deduct_limit = True
auto_activate = False
else:
user.fido_sign_count = new_sign_count
Session.commit()
del session[MFA_USER_ID]
session["sudo_time"] = int(time())
login_user(user)
flash("Welcome back!", "success")
# Redirect user to correct page
response = make_response(redirect(next_url or url_for("dashboard.index")))
if fido_token_form.remember.data:
browser = MfaBrowser.create_new(user=user)
Session.commit()
response.set_cookie(
"mfa",
value=browser.token,
expires=browser.expires.datetime,
secure=True if URL.startswith("https") else False,
httponly=True,
samesite="Lax",
)
return response
# Prepare information for key registration process
session.pop("challenge", None)
challenge = secrets.token_urlsafe(32)
session["fido_challenge"] = challenge.rstrip("=")
fidos = Fido.filter_by(uuid=user.fido_uuid).all()
webauthn_users = []
for fido in fidos:
webauthn_users.append(
webauthn.WebAuthnUser(
user.fido_uuid,
user.email,
user.name if user.name else user.email,
False,
fido.credential_id,
fido.public_key,
fido.sign_count,
RP_ID,
)
)
webauthn_assertion_options = webauthn.WebAuthnAssertionOptions(
webauthn_users, challenge
)
webauthn_assertion_options = webauthn_assertion_options.assertion_dict
try:
# HACK: We need to upgrade to webauthn > 1 so it can support specifying the transports
for credential in webauthn_assertion_options["allowCredentials"]:
del credential["transports"]
except KeyError:
# Should never happen but...
pass
return render_template(
"auth/fido.html",
fido_token_form=fido_token_form,
webauthn_assertion_options=webauthn_assertion_options,
enable_otp=user.enable_otp,
auto_activate=auto_activate,
next_url=next_url,
)

View File

@ -1,10 +1,13 @@
from flask import request, render_template, redirect, url_for from flask import request, render_template, flash, g
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, validators from wtforms import StringField, validators
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.dashboard.views.setting import send_reset_password_email from app.dashboard.views.account_setting import send_reset_password_email
from app.extensions import limiter
from app.log import LOG
from app.models import User from app.models import User
from app.utils import sanitize_email, canonicalize_email
class ForgotPasswordForm(FlaskForm): class ForgotPasswordForm(FlaskForm):
@ -12,19 +15,27 @@ class ForgotPasswordForm(FlaskForm):
@auth_bp.route("/forgot_password", methods=["GET", "POST"]) @auth_bp.route("/forgot_password", methods=["GET", "POST"])
@limiter.limit(
"10/hour", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
)
def forgot_password(): def forgot_password():
form = ForgotPasswordForm(request.form) form = ForgotPasswordForm(request.form)
if form.validate_on_submit(): if form.validate_on_submit():
email = form.email.data # Trigger rate limiter
g.deduct_limit = True
user = User.get_by(email=email) flash(
"If your email is correct, you are going to receive an email to reset your password",
"success",
)
if not user: email = sanitize_email(form.email.data)
error = "No such user, are you sure the email is correct?" canonical_email = canonicalize_email(email)
return render_template("auth/forgot_password.html", form=form, error=error) user = User.get_by(email=email) or User.get_by(email=canonical_email)
send_reset_password_email(user) if user:
return redirect(url_for("auth.forgot_password")) LOG.d("Send forgot password email to %s", user)
send_reset_password_email(user)
return render_template("auth/forgot_password.html", form=form) return render_template("auth/forgot_password.html", form=form)

View File

@ -1,16 +1,13 @@
from flask import request, session, redirect, flash, url_for from flask import request, session, redirect, flash, url_for
from flask_login import login_user
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from app import email_utils
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.auth.views.login_utils import after_login from app.auth.views.login_utils import after_login
from app.config import GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, URL, DISABLE_REGISTRATION from app.config import GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, URL
from app.email_utils import can_be_used_as_personal_email, email_already_used from app.db import Session
from app.extensions import db
from app.log import LOG from app.log import LOG
from app.models import User, SocialAuth from app.models import User, SocialAuth
from app.utils import encode_url from app.utils import encode_url, sanitize_email, sanitize_next_url
_authorization_base_url = "https://github.com/login/oauth/authorize" _authorization_base_url = "https://github.com/login/oauth/authorize"
_token_url = "https://github.com/login/oauth/access_token" _token_url = "https://github.com/login/oauth/access_token"
@ -22,7 +19,7 @@ _redirect_uri = URL + "/auth/github/callback"
@auth_bp.route("/github/login") @auth_bp.route("/github/login")
def github_login(): def github_login():
next_url = request.args.get("next") next_url = sanitize_next_url(request.args.get("next"))
if next_url: if next_url:
redirect_uri = _redirect_uri + "?next=" + encode_url(next_url) redirect_uri = _redirect_uri + "?next=" + encode_url(next_url)
else: else:
@ -51,7 +48,7 @@ def github_callback():
scope=["user:email"], scope=["user:email"],
redirect_uri=_redirect_uri, redirect_uri=_redirect_uri,
) )
token = github.fetch_token( github.fetch_token(
_token_url, _token_url,
client_secret=GITHUB_CLIENT_SECRET, client_secret=GITHUB_CLIENT_SECRET,
authorization_response=request.url, authorization_response=request.url,
@ -78,41 +75,28 @@ def github_callback():
break break
if not email: if not email:
LOG.error(f"cannot get email for github user {github_user_data} {emails}") LOG.e(f"cannot get email for github user {github_user_data} {emails}")
flash( flash(
"Cannot get a valid email from Github, please another way to login/sign up", "Cannot get a valid email from Github, please another way to login/sign up",
"error", "error",
) )
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
email = email.lower() email = sanitize_email(email)
user = User.get_by(email=email) user = User.get_by(email=email)
# create user
if not user: if not user:
if DISABLE_REGISTRATION: flash(
flash("Registration is closed", "error") "Sorry you cannot sign up via Github, please use email/password sign-up instead",
return redirect(url_for("auth.login")) "error",
if not can_be_used_as_personal_email(email) or email_already_used(email):
flash(f"You cannot use {email} as your personal inbox.", "error")
return redirect(url_for("auth.login"))
LOG.d("create github user")
user = User.create(
email=email.lower(), name=github_user_data.get("name") or "", activated=True
) )
db.session.commit() return redirect(url_for("auth.register"))
login_user(user)
email_utils.send_welcome_email(user)
flash(f"Welcome to SimpleLogin {user.name}!", "success")
if not SocialAuth.get_by(user_id=user.id, social="github"): if not SocialAuth.get_by(user_id=user.id, social="github"):
SocialAuth.create(user_id=user.id, social="github") SocialAuth.create(user_id=user.id, social="github")
db.session.commit() Session.commit()
# The activation link contains the original page, for ex authorize page # The activation link contains the original page, for ex authorize page
next_url = request.args.get("next") if request.args else None next_url = sanitize_next_url(request.args.get("next")) if request.args else None
return after_login(user, next_url) return after_login(user, next_url)

View File

@ -1,16 +1,14 @@
from flask import request, session, redirect, flash, url_for from flask import request, session, redirect, flash, url_for
from flask_login import login_user
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from app import s3, email_utils from app import s3
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.config import URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, DISABLE_REGISTRATION from app.config import URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
from app.extensions import db from app.db import Session
from app.log import LOG from app.log import LOG
from app.models import User, File, SocialAuth from app.models import User, File, SocialAuth
from app.utils import random_string from app.utils import random_string, sanitize_email, sanitize_next_url
from .login_utils import after_login from .login_utils import after_login
from ...email_utils import can_be_used_as_personal_email, email_already_used
_authorization_base_url = "https://accounts.google.com/o/oauth2/v2/auth" _authorization_base_url = "https://accounts.google.com/o/oauth2/v2/auth"
_token_url = "https://www.googleapis.com/oauth2/v4/token" _token_url = "https://www.googleapis.com/oauth2/v4/token"
@ -31,7 +29,7 @@ def google_login():
# to avoid flask-login displaying the login error message # to avoid flask-login displaying the login error message
session.pop("_flashes", None) session.pop("_flashes", None)
next_url = request.args.get("next") next_url = sanitize_next_url(request.args.get("next"))
# Google does not allow to append param to redirect_url # Google does not allow to append param to redirect_url
# we need to pass the next url by session # we need to pass the next url by session
@ -55,11 +53,12 @@ def google_callback():
google = OAuth2Session( google = OAuth2Session(
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_ID,
state=session["oauth_state"], # some how Google Login fails with oauth_state KeyError
# state=session["oauth_state"],
scope=_scope, scope=_scope,
redirect_uri=_redirect_uri, redirect_uri=_redirect_uri,
) )
token = google.fetch_token( google.fetch_token(
_token_url, _token_url,
client_secret=GOOGLE_CLIENT_SECRET, client_secret=GOOGLE_CLIENT_SECRET,
authorization_response=request.url, authorization_response=request.url,
@ -80,7 +79,7 @@ def google_callback():
"https://www.googleapis.com/oauth2/v1/userinfo" "https://www.googleapis.com/oauth2/v1/userinfo"
).json() ).json()
email = google_user_data["email"] email = sanitize_email(google_user_data["email"])
user = User.get_by(email=email) user = User.get_by(email=email)
picture_url = google_user_data.get("picture") picture_url = google_user_data.get("picture")
@ -88,58 +87,39 @@ def google_callback():
if user: if user:
if picture_url and not user.profile_picture_id: if picture_url and not user.profile_picture_id:
LOG.d("set user profile picture to %s", picture_url) LOG.d("set user profile picture to %s", picture_url)
file = create_file_from_url(picture_url) file = create_file_from_url(user, picture_url)
user.profile_picture_id = file.id user.profile_picture_id = file.id
db.session.commit() Session.commit()
# create user
else: else:
if DISABLE_REGISTRATION: flash(
flash("Registration is closed", "error") "Sorry you cannot sign up via Google, please use email/password sign-up instead",
return redirect(url_for("auth.login")) "error",
if not can_be_used_as_personal_email(email) or email_already_used(email):
flash(f"You cannot use {email} as your personal inbox.", "error")
return redirect(url_for("auth.login"))
LOG.d("create google user with %s", google_user_data)
user = User.create(
email=email.lower(), name=google_user_data["name"], activated=True
) )
return redirect(url_for("auth.register"))
if picture_url:
LOG.d("set user profile picture to %s", picture_url)
file = create_file_from_url(picture_url)
user.profile_picture_id = file.id
db.session.commit()
login_user(user)
email_utils.send_welcome_email(user)
flash(f"Welcome to SimpleLogin {user.name}!", "success")
next_url = None next_url = None
# The activation link contains the original page, for ex authorize page # The activation link contains the original page, for ex authorize page
if "google_next_url" in session: if "google_next_url" in session:
next_url = session["google_next_url"] next_url = session["google_next_url"]
LOG.debug("redirect user to %s", next_url) LOG.d("redirect user to %s", next_url)
# reset the next_url to avoid user getting redirected at each login :) # reset the next_url to avoid user getting redirected at each login :)
session.pop("google_next_url", None) session.pop("google_next_url", None)
if not SocialAuth.get_by(user_id=user.id, social="google"): if not SocialAuth.get_by(user_id=user.id, social="google"):
SocialAuth.create(user_id=user.id, social="google") SocialAuth.create(user_id=user.id, social="google")
db.session.commit() Session.commit()
return after_login(user, next_url) return after_login(user, next_url)
def create_file_from_url(url) -> File: def create_file_from_url(user, url) -> File:
file_path = random_string(30) file_path = random_string(30)
file = File.create(path=file_path) file = File.create(path=file_path, user_id=user.id)
s3.upload_from_url(url, file_path) s3.upload_from_url(url, file_path)
db.session.flush() Session.flush()
LOG.d("upload file %s to s3", file) LOG.d("upload file %s to s3", file)
return file return file

View File

@ -1,12 +1,16 @@
from flask import request, render_template, redirect, url_for, flash from flask import request, render_template, redirect, url_for, flash, g
from flask_login import current_user from flask_login import current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, validators from wtforms import StringField, validators
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.auth.views.login_utils import after_login from app.auth.views.login_utils import after_login
from app.config import CONNECT_WITH_PROTON, CONNECT_WITH_OIDC_ICON, OIDC_CLIENT_ID
from app.events.auth_event import LoginEvent
from app.extensions import limiter
from app.log import LOG from app.log import LOG
from app.models import User from app.models import User
from app.utils import sanitize_email, sanitize_next_url, canonicalize_email
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
@ -15,29 +19,56 @@ class LoginForm(FlaskForm):
@auth_bp.route("/login", methods=["GET", "POST"]) @auth_bp.route("/login", methods=["GET", "POST"])
@limiter.limit(
"10/minute", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
)
def login(): def login():
next_url = sanitize_next_url(request.args.get("next"))
if current_user.is_authenticated: if current_user.is_authenticated:
LOG.d("user is already authenticated, redirect to dashboard") if next_url:
return redirect(url_for("dashboard.index")) LOG.d("user is already authenticated, redirect to %s", next_url)
return redirect(next_url)
else:
LOG.d("user is already authenticated, redirect to dashboard")
return redirect(url_for("dashboard.index"))
form = LoginForm(request.form) form = LoginForm(request.form)
next_url = request.args.get("next")
show_resend_activation = False show_resend_activation = False
if form.validate_on_submit(): if form.validate_on_submit():
user = User.filter_by(email=form.email.data).first() email = sanitize_email(form.email.data)
canonical_email = canonicalize_email(email)
user = User.get_by(email=email) or User.get_by(email=canonical_email)
if not user: if not user or not user.check_password(form.password.data):
flash("Email or password incorrect", "error") # Trigger rate limiter
elif not user.check_password(form.password.data): g.deduct_limit = True
form.password.data = None
flash("Email or password incorrect", "error") flash("Email or password incorrect", "error")
LoginEvent(LoginEvent.ActionType.failed).send()
elif user.disabled:
flash(
"Your account is disabled. Please contact SimpleLogin team to re-enable your account.",
"error",
)
LoginEvent(LoginEvent.ActionType.disabled_login).send()
elif user.delete_on is not None:
flash(
f"Your account is scheduled to be deleted on {user.delete_on}",
"error",
)
LoginEvent(LoginEvent.ActionType.scheduled_to_be_deleted).send()
elif not user.activated: elif not user.activated:
show_resend_activation = True show_resend_activation = True
flash( flash(
"Please check your inbox for the activation email. You can also have this email re-sent", "Please check your inbox for the activation email. You can also have this email re-sent",
"error", "error",
) )
LoginEvent(LoginEvent.ActionType.not_activated).send()
else: else:
LoginEvent(LoginEvent.ActionType.success).send()
return after_login(user, next_url) return after_login(user, next_url)
return render_template( return render_template(
@ -45,4 +76,7 @@ def login():
form=form, form=form,
next_url=next_url, next_url=next_url,
show_resend_activation=show_resend_activation, show_resend_activation=show_resend_activation,
connect_with_proton=CONNECT_WITH_PROTON,
connect_with_oidc=OIDC_CLIENT_ID is not None,
connect_with_oidc_icon=CONNECT_WITH_OIDC_ICON,
) )

View File

@ -1,30 +1,68 @@
from flask import session, redirect, url_for from time import time
from typing import Optional
from flask import session, redirect, url_for, request
from flask_login import login_user from flask_login import login_user
from app.config import MFA_USER_ID from app.config import MFA_USER_ID
from app.log import LOG from app.log import LOG
from app.models import Referral
def after_login(user, next_url): def after_login(user, next_url, login_from_proton: bool = False):
""" """
Redirect to the correct page after login. Redirect to the correct page after login.
If the user is logged in with Proton, do not look at fido nor otp
If user enables MFA: redirect user to MFA page If user enables MFA: redirect user to MFA page
Otherwise redirect to dashboard page if no next_url Otherwise redirect to dashboard page if no next_url
""" """
if user.enable_otp: if not login_from_proton:
session[MFA_USER_ID] = user.id if user.fido_enabled():
if next_url: # Use the same session for FIDO so that we can easily
return redirect(url_for("auth.mfa", next_url=next_url)) # switch between these two 2FA option
else: session[MFA_USER_ID] = user.id
return redirect(url_for("auth.mfa")) if next_url:
else: return redirect(url_for("auth.fido", next=next_url))
LOG.debug("log user %s in", user) else:
login_user(user) return redirect(url_for("auth.fido"))
elif user.enable_otp:
session[MFA_USER_ID] = user.id
if next_url:
return redirect(url_for("auth.mfa", next=next_url))
else:
return redirect(url_for("auth.mfa"))
# User comes to login page from another page LOG.d("log user %s in", user)
if next_url: login_user(user)
LOG.debug("redirect user to %s", next_url) session["sudo_time"] = int(time())
return redirect(next_url)
else: # User comes to login page from another page
LOG.debug("redirect user to dashboard") if next_url:
return redirect(url_for("dashboard.index")) LOG.d("redirect user to %s", next_url)
return redirect(next_url)
else:
LOG.d("redirect user to dashboard")
return redirect(url_for("dashboard.index"))
# name of the cookie that stores the referral code
_REFERRAL_COOKIE = "slref"
def get_referral() -> Optional[Referral]:
"""Get the eventual referral stored in cookie"""
# whether user arrives via a referral
referral = None
if request.cookies:
ref_code = request.cookies.get(_REFERRAL_COOKIE)
referral = Referral.get_by(code=ref_code)
if not referral:
if "slref" in session:
ref_code = session["slref"]
referral = Referral.get_by(code=ref_code)
if referral:
LOG.d("referral found %s", referral)
return referral

View File

@ -1,10 +1,17 @@
from flask import render_template from flask import redirect, url_for, flash, make_response
from flask_login import logout_user
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.config import SESSION_COOKIE_NAME
from app.session import logout_session
@auth_bp.route("/logout") @auth_bp.route("/logout")
def logout(): def logout():
logout_user() logout_session()
return render_template("auth/logout.html") flash("You are logged out", "success")
response = make_response(redirect(url_for("auth.login")))
response.delete_cookie(SESSION_COOKIE_NAME)
response.delete_cookie("mfa")
response.delete_cookie("dark-mode")
return response

View File

@ -1,20 +1,38 @@
import pyotp import pyotp
from flask import request, render_template, redirect, url_for, flash, session from flask import (
render_template,
redirect,
url_for,
flash,
session,
make_response,
request,
g,
)
from flask_login import login_user from flask_login import login_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, validators from wtforms import BooleanField, StringField, validators
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.config import MFA_USER_ID from app.config import MFA_USER_ID, URL
from app.log import LOG from app.db import Session
from app.models import User from app.email_utils import send_invalid_totp_login_email
from app.extensions import limiter
from app.models import User, MfaBrowser
from app.utils import sanitize_next_url
class OtpTokenForm(FlaskForm): class OtpTokenForm(FlaskForm):
token = StringField("Token", validators=[validators.DataRequired()]) token = StringField("Token", validators=[validators.DataRequired()])
remember = BooleanField(
"attr", default=False, description="Remember this browser for 30 days"
)
@auth_bp.route("/mfa", methods=["GET", "POST"]) @auth_bp.route("/mfa", methods=["GET", "POST"])
@limiter.limit(
"10/minute", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
)
def mfa(): def mfa():
# passed from login page # passed from login page
user_id = session.get(MFA_USER_ID) user_id = session.get(MFA_USER_ID)
@ -31,28 +49,59 @@ def mfa():
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
otp_token_form = OtpTokenForm() otp_token_form = OtpTokenForm()
next_url = request.args.get("next") next_url = sanitize_next_url(request.args.get("next"))
if request.cookies.get("mfa"):
browser = MfaBrowser.get_by(token=request.cookies.get("mfa"))
if browser and not browser.is_expired() and browser.user_id == user.id:
login_user(user)
flash("Welcome back!", "success")
# Redirect user to correct page
return redirect(next_url or url_for("dashboard.index"))
else:
# Trigger rate limiter
g.deduct_limit = True
if otp_token_form.validate_on_submit(): if otp_token_form.validate_on_submit():
totp = pyotp.TOTP(user.otp_secret) totp = pyotp.TOTP(user.otp_secret)
token = otp_token_form.token.data token = otp_token_form.token.data.replace(" ", "")
if totp.verify(token): if totp.verify(token, valid_window=2) and user.last_otp != token:
del session[MFA_USER_ID] del session[MFA_USER_ID]
user.last_otp = token
Session.commit()
login_user(user) login_user(user)
flash(f"Welcome back {user.name}!") flash("Welcome back!", "success")
# User comes to login page from another page # Redirect user to correct page
if next_url: response = make_response(redirect(next_url or url_for("dashboard.index")))
LOG.debug("redirect user to %s", next_url)
return redirect(next_url) if otp_token_form.remember.data:
else: browser = MfaBrowser.create_new(user=user)
LOG.debug("redirect user to dashboard") Session.commit()
return redirect(url_for("dashboard.index")) response.set_cookie(
"mfa",
value=browser.token,
expires=browser.expires.datetime,
secure=True if URL.startswith("https") else False,
httponly=True,
samesite="Lax",
)
return response
else: else:
flash("Incorrect token", "warning") flash("Incorrect token", "warning")
# Trigger rate limiter
g.deduct_limit = True
otp_token_form.token.data = None
send_invalid_totp_login_email(user, "TOTP")
return render_template("auth/mfa.html", otp_token_form=otp_token_form) return render_template(
"auth/mfa.html",
otp_token_form=otp_token_form,
enable_fido=(user.fido_enabled()),
next_url=next_url,
)

135
app/auth/views/oidc.py Normal file
View File

@ -0,0 +1,135 @@
from flask import request, session, redirect, flash, url_for
from requests_oauthlib import OAuth2Session
import requests
from app import config
from app.auth.base import auth_bp
from app.auth.views.login_utils import after_login
from app.config import (
URL,
OIDC_SCOPES,
OIDC_NAME_FIELD,
)
from app.db import Session
from app.email_utils import send_welcome_email
from app.log import LOG
from app.models import User, SocialAuth
from app.utils import sanitize_email, sanitize_next_url
# need to set explicitly redirect_uri instead of leaving the lib to pre-fill redirect_uri
# when served behind nginx, the redirect_uri is localhost... and not the real url
redirect_uri = URL + "/auth/oidc/callback"
SESSION_STATE_KEY = "oauth_state"
SESSION_NEXT_KEY = "oauth_redirect_next"
@auth_bp.route("/oidc/login")
def oidc_login():
if config.OIDC_CLIENT_ID is None or config.OIDC_CLIENT_SECRET is None:
return redirect(url_for("auth.login"))
next_url = sanitize_next_url(request.args.get("next"))
auth_url = requests.get(config.OIDC_WELL_KNOWN_URL).json()["authorization_endpoint"]
oidc = OAuth2Session(
config.OIDC_CLIENT_ID, scope=[OIDC_SCOPES], redirect_uri=redirect_uri
)
authorization_url, state = oidc.authorization_url(auth_url)
# State is used to prevent CSRF, keep this for later.
session[SESSION_STATE_KEY] = state
session[SESSION_NEXT_KEY] = next_url
return redirect(authorization_url)
@auth_bp.route("/oidc/callback")
def oidc_callback():
if SESSION_STATE_KEY not in session:
flash("Invalid state, please retry", "error")
return redirect(url_for("auth.login"))
if config.OIDC_CLIENT_ID is None or config.OIDC_CLIENT_SECRET is None:
return redirect(url_for("auth.login"))
# user clicks on cancel
if "error" in request.args:
flash("Please use another sign in method then", "warning")
return redirect("/")
oidc_configuration = requests.get(config.OIDC_WELL_KNOWN_URL).json()
user_info_url = oidc_configuration["userinfo_endpoint"]
token_url = oidc_configuration["token_endpoint"]
oidc = OAuth2Session(
config.OIDC_CLIENT_ID,
state=session[SESSION_STATE_KEY],
scope=[OIDC_SCOPES],
redirect_uri=redirect_uri,
)
oidc.fetch_token(
token_url,
client_secret=config.OIDC_CLIENT_SECRET,
authorization_response=request.url,
)
oidc_user_data = oidc.get(user_info_url)
if oidc_user_data.status_code != 200:
LOG.e(
f"cannot get oidc user data {oidc_user_data.status_code} {oidc_user_data.text}"
)
flash(
"Cannot get user data from OIDC, please use another way to login/sign up",
"error",
)
return redirect(url_for("auth.login"))
oidc_user_data = oidc_user_data.json()
email = oidc_user_data.get("email")
if not email:
LOG.e(f"cannot get email for OIDC user {oidc_user_data} {email}")
flash(
"Cannot get a valid email from OIDC, please another way to login/sign up",
"error",
)
return redirect(url_for("auth.login"))
email = sanitize_email(email)
user = User.get_by(email=email)
if not user and config.DISABLE_REGISTRATION:
flash(
"Sorry you cannot sign up via the OIDC provider. Please sign-up first with your email.",
"error",
)
return redirect(url_for("auth.register"))
elif not user:
user = create_user(email, oidc_user_data)
if not SocialAuth.get_by(user_id=user.id, social="oidc"):
SocialAuth.create(user_id=user.id, social="oidc")
Session.commit()
# The activation link contains the original page, for ex authorize page
next_url = session[SESSION_NEXT_KEY]
session[SESSION_NEXT_KEY] = None
return after_login(user, next_url)
def create_user(email, oidc_user_data):
new_user = User.create(
email=email,
name=oidc_user_data.get(OIDC_NAME_FIELD),
password="",
activated=True,
)
LOG.i(f"Created new user for login request from OIDC. New user {new_user.id}")
Session.commit()
send_welcome_email(new_user)
return new_user

190
app/auth/views/proton.py Normal file
View File

@ -0,0 +1,190 @@
import requests
from flask import request, session, redirect, flash, url_for
from flask_limiter.util import get_remote_address
from flask_login import current_user
from requests_oauthlib import OAuth2Session
from typing import Optional
from app.auth.base import auth_bp
from app.auth.views.login_utils import after_login
from app.config import (
PROTON_BASE_URL,
PROTON_CLIENT_ID,
PROTON_CLIENT_SECRET,
PROTON_EXTRA_HEADER_NAME,
PROTON_EXTRA_HEADER_VALUE,
PROTON_VALIDATE_CERTS,
URL,
)
from app.log import LOG
from app.models import ApiKey, User
from app.proton.proton_client import HttpProtonClient, convert_access_token
from app.proton.proton_callback_handler import (
ProtonCallbackHandler,
Action,
)
from app.proton.utils import get_proton_partner
from app.utils import sanitize_next_url, sanitize_scheme
_authorization_base_url = PROTON_BASE_URL + "/oauth/authorize"
_token_url = PROTON_BASE_URL + "/oauth/token"
# need to set explicitly redirect_uri instead of leaving the lib to pre-fill redirect_uri
# when served behind nginx, the redirect_uri is localhost... and not the real url
_redirect_uri = URL + "/auth/proton/callback"
SESSION_ACTION_KEY = "oauth_action"
SESSION_STATE_KEY = "oauth_state"
DEFAULT_SCHEME = "auth.simplelogin"
def get_api_key_for_user(user: User) -> str:
ak = ApiKey.create(
user_id=user.id,
name="Created via Login with Proton on mobile app",
commit=True,
)
return ak.code
def extract_action() -> Optional[Action]:
action = request.args.get("action")
if action is not None:
if action == "link":
return Action.Link
elif action == "login":
return Action.Login
else:
LOG.w(f"Unknown action received: {action}")
return None
return Action.Login
def get_action_from_state() -> Action:
oauth_action = session[SESSION_ACTION_KEY]
if oauth_action == Action.Login.value:
return Action.Login
elif oauth_action == Action.Link.value:
return Action.Link
raise Exception(f"Unknown action in state: {oauth_action}")
@auth_bp.route("/proton/login")
def proton_login():
if PROTON_CLIENT_ID is None or PROTON_CLIENT_SECRET is None:
return redirect(url_for("auth.login"))
action = extract_action()
if action is None:
return redirect(url_for("auth.login"))
if action == Action.Link and not current_user.is_authenticated:
return redirect(url_for("auth.login"))
next_url = sanitize_next_url(request.args.get("next"))
if next_url:
session["oauth_next"] = next_url
elif "oauth_next" in session:
del session["oauth_next"]
scheme = sanitize_scheme(request.args.get("scheme"))
if scheme:
session["oauth_scheme"] = scheme
elif "oauth_scheme" in session:
del session["oauth_scheme"]
mode = request.args.get("mode", "session")
if mode == "apikey":
session["oauth_mode"] = "apikey"
else:
session["oauth_mode"] = "session"
proton = OAuth2Session(PROTON_CLIENT_ID, redirect_uri=_redirect_uri)
authorization_url, state = proton.authorization_url(_authorization_base_url)
# State is used to prevent CSRF, keep this for later.
session[SESSION_STATE_KEY] = state
session[SESSION_ACTION_KEY] = action.value
return redirect(authorization_url)
@auth_bp.route("/proton/callback")
def proton_callback():
if SESSION_STATE_KEY not in session or SESSION_STATE_KEY not in session:
flash("Invalid state, please retry", "error")
return redirect(url_for("auth.login"))
if PROTON_CLIENT_ID is None or PROTON_CLIENT_SECRET is None:
return redirect(url_for("auth.login"))
# user clicks on cancel
if "error" in request.args:
flash("Please use another sign in method then", "warning")
return redirect("/")
proton = OAuth2Session(
PROTON_CLIENT_ID,
state=session[SESSION_STATE_KEY],
redirect_uri=_redirect_uri,
)
def check_status_code(response: requests.Response) -> requests.Response:
if response.status_code != 200:
raise Exception(
f"Bad Proton API response [status={response.status_code}]: {response.json()}"
)
return response
proton.register_compliance_hook("access_token_response", check_status_code)
headers = None
if PROTON_EXTRA_HEADER_NAME and PROTON_EXTRA_HEADER_VALUE:
headers = {PROTON_EXTRA_HEADER_NAME: PROTON_EXTRA_HEADER_VALUE}
try:
token = proton.fetch_token(
_token_url,
client_secret=PROTON_CLIENT_SECRET,
authorization_response=request.url,
verify=PROTON_VALIDATE_CERTS,
method="GET",
include_client_id=True,
headers=headers,
)
except Exception as e:
LOG.warning(f"Error fetching Proton token: {e}")
flash("There was an error in the login process", "error")
return redirect(url_for("auth.login"))
credentials = convert_access_token(token["access_token"])
action = get_action_from_state()
proton_client = HttpProtonClient(
PROTON_BASE_URL, credentials, get_remote_address(), verify=PROTON_VALIDATE_CERTS
)
handler = ProtonCallbackHandler(proton_client)
proton_partner = get_proton_partner()
next_url = session.get("oauth_next")
if action == Action.Login:
res = handler.handle_login(proton_partner)
elif action == Action.Link:
res = handler.handle_link(current_user, proton_partner)
else:
raise Exception(f"Unknown Action: {action.name}")
if res.flash_message is not None:
flash(res.flash_message, res.flash_category)
oauth_scheme = session.get("oauth_scheme")
if session.get("oauth_mode", "session") == "apikey":
apikey = get_api_key_for_user(res.user)
scheme = oauth_scheme or DEFAULT_SCHEME
return redirect(f"{scheme}:///login?apikey={apikey}")
if res.redirect_to_login:
return redirect(url_for("auth.login"))
if next_url and next_url[0] == "/" and oauth_scheme:
next_url = f"{oauth_scheme}://{next_url}"
redirect_url = next_url or res.redirect
return after_login(res.user, redirect_url, login_from_proton=True)

View File

@ -0,0 +1,75 @@
import arrow
from flask import request, render_template, redirect, url_for, flash, session, g
from flask_login import login_user
from flask_wtf import FlaskForm
from wtforms import StringField, validators
from app.auth.base import auth_bp
from app.config import MFA_USER_ID
from app.db import Session
from app.email_utils import send_invalid_totp_login_email
from app.extensions import limiter
from app.log import LOG
from app.models import User, RecoveryCode
from app.utils import sanitize_next_url
class RecoveryForm(FlaskForm):
code = StringField("Code", validators=[validators.DataRequired()])
@auth_bp.route("/recovery", methods=["GET", "POST"])
@limiter.limit(
"10/minute", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
)
def recovery_route():
# passed from login page
user_id = session.get(MFA_USER_ID)
# user access this page directly without passing by login page
if not user_id:
flash("Unknown error, redirect back to main page", "warning")
return redirect(url_for("auth.login"))
user = User.get(user_id)
if not user.two_factor_authentication_enabled():
flash("Only user with MFA enabled should go to this page", "warning")
return redirect(url_for("auth.login"))
recovery_form = RecoveryForm()
next_url = sanitize_next_url(request.args.get("next"))
if recovery_form.validate_on_submit():
code = recovery_form.code.data
recovery_code = RecoveryCode.find_by_user_code(user, code)
if recovery_code:
if recovery_code.used:
# Trigger rate limiter
g.deduct_limit = True
flash("Code already used", "error")
else:
del session[MFA_USER_ID]
login_user(user)
flash("Welcome back!", "success")
recovery_code.used = True
recovery_code.used_at = arrow.now()
Session.commit()
# User comes to login page from another page
if next_url:
LOG.d("redirect user to %s", next_url)
return redirect(next_url)
else:
LOG.d("redirect user to dashboard")
return redirect(url_for("dashboard.index"))
else:
# Trigger rate limiter
g.deduct_limit = True
flash("Incorrect code", "error")
send_invalid_totp_login_email(user, "recovery")
return render_template("auth/recovery.html", recovery_form=recovery_form)

View File

@ -1,3 +1,4 @@
import requests
from flask import request, flash, render_template, redirect, url_for from flask import request, flash, render_template, redirect, url_for
from flask_login import current_user from flask_login import current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
@ -5,18 +6,25 @@ from wtforms import StringField, validators
from app import email_utils, config from app import email_utils, config
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.config import URL, DISABLE_REGISTRATION from app.config import CONNECT_WITH_PROTON, CONNECT_WITH_OIDC_ICON
from app.email_utils import can_be_used_as_personal_email, email_already_used from app.auth.views.login_utils import get_referral
from app.extensions import db from app.config import URL, HCAPTCHA_SECRET, HCAPTCHA_SITEKEY
from app.db import Session
from app.email_utils import (
email_can_be_used_as_mailbox,
personal_email_already_used,
)
from app.events.auth_event import RegisterEvent
from app.log import LOG from app.log import LOG
from app.models import User, ActivationCode from app.models import User, ActivationCode, DailyMetric
from app.utils import random_string, encode_url from app.utils import random_string, encode_url, sanitize_email, canonicalize_email
class RegisterForm(FlaskForm): class RegisterForm(FlaskForm):
email = StringField("Email", validators=[validators.DataRequired()]) email = StringField("Email", validators=[validators.DataRequired()])
password = StringField( password = StringField(
"Password", validators=[validators.DataRequired(), validators.Length(min=8)] "Password",
validators=[validators.DataRequired(), validators.Length(min=8, max=100)],
) )
@ -35,28 +43,82 @@ def register():
next_url = request.args.get("next") next_url = request.args.get("next")
if form.validate_on_submit(): if form.validate_on_submit():
email = form.email.data.lower() # only check if hcaptcha is enabled
if not can_be_used_as_personal_email(email): if HCAPTCHA_SECRET:
flash("You cannot use this email address as your personal inbox.", "error") # check with hCaptcha
else: token = request.form.get("h-captcha-response")
if email_already_used(email): params = {"secret": HCAPTCHA_SECRET, "response": token}
flash(f"Email {email} already used", "error") hcaptcha_res = requests.post(
else: "https://hcaptcha.com/siteverify", data=params
LOG.debug("create user %s", form.email.data) ).json()
user = User.create(email=email, name="", password=form.password.data) # return something like
db.session.commit() # {'success': True,
# 'challenge_ts': '2020-07-23T10:03:25',
# 'hostname': '127.0.0.1'}
if not hcaptcha_res["success"]:
LOG.w(
"User put wrong captcha %s %s",
form.email.data,
hcaptcha_res,
)
flash("Wrong Captcha", "error")
RegisterEvent(RegisterEvent.ActionType.catpcha_failed).send()
return render_template(
"auth/register.html",
form=form,
next_url=next_url,
HCAPTCHA_SITEKEY=HCAPTCHA_SITEKEY,
)
send_activation_email(user, next_url) email = canonicalize_email(form.email.data)
if not email_can_be_used_as_mailbox(email):
flash("You cannot use this email address as your personal inbox.", "error")
RegisterEvent(RegisterEvent.ActionType.email_in_use).send()
else:
sanitized_email = sanitize_email(form.email.data)
if personal_email_already_used(email) or personal_email_already_used(
sanitized_email
):
flash(f"Email {email} already used", "error")
RegisterEvent(RegisterEvent.ActionType.email_in_use).send()
else:
LOG.d("create user %s", email)
user = User.create(
email=email,
name=form.email.data,
password=form.password.data,
referral=get_referral(),
)
Session.commit()
try:
send_activation_email(user, next_url)
RegisterEvent(RegisterEvent.ActionType.success).send()
DailyMetric.get_or_create_today_metric().nb_new_web_non_proton_user += 1
Session.commit()
except Exception:
flash("Invalid email, are you sure the email is correct?", "error")
RegisterEvent(RegisterEvent.ActionType.invalid_email).send()
return redirect(url_for("auth.register"))
return render_template("auth/register_waiting_activation.html") return render_template("auth/register_waiting_activation.html")
return render_template("auth/register.html", form=form, next_url=next_url) return render_template(
"auth/register.html",
form=form,
next_url=next_url,
HCAPTCHA_SITEKEY=HCAPTCHA_SITEKEY,
connect_with_proton=CONNECT_WITH_PROTON,
connect_with_oidc=config.OIDC_CLIENT_ID is not None,
connect_with_oidc_icon=CONNECT_WITH_OIDC_ICON,
)
def send_activation_email(user, next_url): def send_activation_email(user, next_url):
# the activation code is valid for 1h # the activation code is valid for 1h and delete all previous codes
Session.query(ActivationCode).filter(ActivationCode.user_id == user.id).delete()
activation = ActivationCode.create(user_id=user.id, code=random_string(30)) activation = ActivationCode.create(user_id=user.id, code=random_string(30))
db.session.commit() Session.commit()
# Send user activation email # Send user activation email
activation_link = f"{URL}/auth/activate?code={activation.code}" activation_link = f"{URL}/auth/activate?code={activation.code}"
@ -64,4 +126,4 @@ def send_activation_email(user, next_url):
LOG.d("redirect user to %s after activation", next_url) LOG.d("redirect user to %s after activation", next_url)
activation_link = activation_link + "&next=" + encode_url(next_url) activation_link = activation_link + "&next=" + encode_url(next_url)
email_utils.send_activation_email(user.email, user.name, activation_link) email_utils.send_activation_email(user, activation_link)

View File

@ -4,8 +4,10 @@ from wtforms import StringField, validators
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.auth.views.register import send_activation_email from app.auth.views.register import send_activation_email
from app.extensions import limiter
from app.log import LOG from app.log import LOG
from app.models import User from app.models import User
from app.utils import sanitize_email, canonicalize_email
class ResendActivationForm(FlaskForm): class ResendActivationForm(FlaskForm):
@ -13,11 +15,14 @@ class ResendActivationForm(FlaskForm):
@auth_bp.route("/resend_activation", methods=["GET", "POST"]) @auth_bp.route("/resend_activation", methods=["GET", "POST"])
@limiter.limit("10/hour")
def resend_activation(): def resend_activation():
form = ResendActivationForm(request.form) form = ResendActivationForm(request.form)
if form.validate_on_submit(): if form.validate_on_submit():
user = User.filter_by(email=form.email.data).first() email = sanitize_email(form.email.data)
canonical_email = canonicalize_email(email)
user = User.get_by(email=email) or User.get_by(email=canonical_email)
if not user: if not user:
flash("There is no such email", "warning") flash("There is no such email", "warning")

View File

@ -1,21 +1,27 @@
import arrow import uuid
from flask import request, flash, render_template, redirect, url_for
from flask_login import login_user from flask import request, flash, render_template, url_for, g
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, validators from wtforms import StringField, validators
from app.auth.base import auth_bp from app.auth.base import auth_bp
from app.extensions import db from app.auth.views.login_utils import after_login
from app.db import Session
from app.extensions import limiter
from app.models import ResetPasswordCode from app.models import ResetPasswordCode
class ResetPasswordForm(FlaskForm): class ResetPasswordForm(FlaskForm):
password = StringField( password = StringField(
"Password", validators=[validators.DataRequired(), validators.Length(min=8)] "Password",
validators=[validators.DataRequired(), validators.Length(min=8, max=100)],
) )
@auth_bp.route("/reset_password", methods=["GET", "POST"]) @auth_bp.route("/reset_password", methods=["GET", "POST"])
@limiter.limit(
"10/minute", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
)
def reset_password(): def reset_password():
form = ResetPasswordForm(request.form) form = ResetPasswordForm(request.form)
@ -26,6 +32,8 @@ def reset_password():
) )
if not reset_password_code: if not reset_password_code:
# Trigger rate limiter
g.deduct_limit = True
error = ( error = (
"The reset password link can be used only once. " "The reset password link can be used only once. "
"Please request a new link to reset password." "Please request a new link to reset password."
@ -38,20 +46,30 @@ def reset_password():
if form.validate_on_submit(): if form.validate_on_submit():
user = reset_password_code.user user = reset_password_code.user
new_password = form.password.data
user.set_password(form.password.data) # avoid user reusing the old password
if user.check_password(new_password):
error = "You cannot reuse the same password"
return render_template("auth/reset_password.html", form=form, error=error)
user.set_password(new_password)
flash("Your new password has been set", "success") flash("Your new password has been set", "success")
# this can be served to activate user too # this can be served to activate user too
user.activated = True user.activated = True
# remove the reset password code # remove all reset password codes
ResetPasswordCode.delete(reset_password_code.id) ResetPasswordCode.filter_by(user_id=user.id).delete()
db.session.commit() # change the alternative_id to log user out on other browsers
login_user(user) user.alternative_id = str(uuid.uuid4())
return redirect(url_for("dashboard.index")) Session.commit()
# do not use login_user(user) here
# to make sure user needs to go through MFA if enabled
return after_login(user, url_for("dashboard.index"))
return render_template("auth/reset_password.html", form=form) return render_template("auth/reset_password.html", form=form)

14
app/auth/views/social.py Normal file
View File

@ -0,0 +1,14 @@
from flask import render_template, redirect, url_for
from flask_login import current_user
from app.auth.base import auth_bp
from app.log import LOG
@auth_bp.route("/social", methods=["GET", "POST"])
def social():
if current_user.is_authenticated:
LOG.d("user is already authenticated, redirect to dashboard")
return redirect(url_for("dashboard.index"))
return render_template("auth/social.html")

2
app/build_info.py Normal file
View File

@ -0,0 +1,2 @@
SHA1 = "dev"
BUILD_TIME = "1652365083"

View File

@ -1,13 +1,13 @@
import os import os
import random import random
import socket
import string import string
import subprocess from ast import literal_eval
import tempfile from typing import Callable, List, Optional
from uuid import uuid4 from urllib.parse import urlparse
from dotenv import load_dotenv from dotenv import load_dotenv
SHA1 = subprocess.getoutput("git rev-parse HEAD")
ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
@ -20,6 +20,48 @@ def get_abs_path(file_path: str):
return os.path.join(ROOT_DIR, file_path) return os.path.join(ROOT_DIR, file_path)
def sl_getenv(env_var: str, default_factory: Callable = None):
"""
Get env value, convert into Python object
Args:
env_var (str): env var, example: SL_DB
default_factory: returns value if this env var is not set.
"""
value = os.getenv(env_var)
if value is None:
return default_factory()
return literal_eval(value)
def get_env_dict(env_var: str) -> dict[str, str]:
"""
Get an env variable and convert it into a python dictionary with keys and values as strings.
Args:
env_var (str): env var, example: SL_DB
Syntax is: key1=value1;key2=value2
Components separated by ;
key and value separated by =
"""
value = os.getenv(env_var)
if not value:
return {}
components = value.split(";")
result = {}
for component in components:
if component == "":
continue
parts = component.split("=")
if len(parts) != 2:
raise Exception(f"Invalid config for env var {env_var}")
result[parts[0].strip()] = parts[1].strip()
return result
config_file = os.environ.get("CONFIG") config_file = os.environ.get("CONFIG")
if config_file: if config_file:
config_file = get_abs_path(config_file) config_file = get_abs_path(config_file)
@ -28,18 +70,18 @@ if config_file:
else: else:
load_dotenv() load_dotenv()
RESET_DB = "RESET_DB" in os.environ
COLOR_LOG = "COLOR_LOG" in os.environ COLOR_LOG = "COLOR_LOG" in os.environ
# Allow user to have 1 year of premium: set the expiration_date to 1 year more # Allow user to have 1 year of premium: set the expiration_date to 1 year more
PROMO_CODE = "SIMPLEISBETTER" PROMO_CODE = "SIMPLEISBETTER"
# Debug mode
DEBUG = os.environ["DEBUG"] if "DEBUG" in os.environ else False
# Server url # Server url
URL = os.environ["URL"] URL = os.environ["URL"]
print(">>> URL:", URL) print(">>> URL:", URL)
# Calculate RP_ID for WebAuthn
RP_ID = urlparse(URL).hostname
SENTRY_DSN = os.environ.get("SENTRY_DSN") SENTRY_DSN = os.environ.get("SENTRY_DSN")
# can use another sentry project for the front-end to avoid noises # can use another sentry project for the front-end to avoid noises
@ -47,105 +89,151 @@ SENTRY_FRONT_END_DSN = os.environ.get("SENTRY_FRONT_END_DSN") or SENTRY_DSN
# Email related settings # Email related settings
NOT_SEND_EMAIL = "NOT_SEND_EMAIL" in os.environ NOT_SEND_EMAIL = "NOT_SEND_EMAIL" in os.environ
EMAIL_DOMAIN = os.environ["EMAIL_DOMAIN"] EMAIL_DOMAIN = os.environ["EMAIL_DOMAIN"].lower()
SUPPORT_EMAIL = os.environ["SUPPORT_EMAIL"] SUPPORT_EMAIL = os.environ["SUPPORT_EMAIL"]
SUPPORT_NAME = os.environ.get("SUPPORT_NAME", "Son from SimpleLogin") SUPPORT_NAME = os.environ.get("SUPPORT_NAME", "Son from SimpleLogin")
ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL") ADMIN_EMAIL = os.environ.get("ADMIN_EMAIL")
# to receive monitoring daily report
MONITORING_EMAIL = os.environ.get("MONITORING_EMAIL")
# VERP: mail_from set to BOUNCE_PREFIX + email_log.id + BOUNCE_SUFFIX
BOUNCE_PREFIX = os.environ.get("BOUNCE_PREFIX") or "bounce+"
BOUNCE_SUFFIX = os.environ.get("BOUNCE_SUFFIX") or f"+@{EMAIL_DOMAIN}"
# Used for VERP during reply phase. It's similar to BOUNCE_PREFIX.
# It's needed when sending emails from custom domain to respect DMARC.
# BOUNCE_PREFIX_FOR_REPLY_PHASE should never be used in any existing alias
# and can't be used for creating a new alias on custom domain
# Note BOUNCE_PREFIX_FOR_REPLY_PHASE doesn't have the trailing plus sign (+) as BOUNCE_PREFIX
BOUNCE_PREFIX_FOR_REPLY_PHASE = (
os.environ.get("BOUNCE_PREFIX_FOR_REPLY_PHASE") or "bounce_reply"
)
# VERP for transactional email: mail_from set to BOUNCE_PREFIX + email_log.id + BOUNCE_SUFFIX
TRANSACTIONAL_BOUNCE_PREFIX = (
os.environ.get("TRANSACTIONAL_BOUNCE_PREFIX") or "transactional+"
)
TRANSACTIONAL_BOUNCE_SUFFIX = (
os.environ.get("TRANSACTIONAL_BOUNCE_SUFFIX") or f"+@{EMAIL_DOMAIN}"
)
try: try:
MAX_NB_EMAIL_FREE_PLAN = int(os.environ["MAX_NB_EMAIL_FREE_PLAN"]) MAX_NB_EMAIL_FREE_PLAN = int(os.environ["MAX_NB_EMAIL_FREE_PLAN"])
except Exception: except Exception:
print("MAX_NB_EMAIL_FREE_PLAN is not set, use 5 as default value") print("MAX_NB_EMAIL_FREE_PLAN is not set, use 5 as default value")
MAX_NB_EMAIL_FREE_PLAN = 5 MAX_NB_EMAIL_FREE_PLAN = 5
# allow to override postfix server locally MAX_NB_EMAIL_OLD_FREE_PLAN = int(os.environ.get("MAX_NB_EMAIL_OLD_FREE_PLAN", 15))
# maximum number of directory a premium user can create
MAX_NB_DIRECTORY = 50
MAX_NB_SUBDOMAIN = 5
ENFORCE_SPF = "ENFORCE_SPF" in os.environ
# override postfix server locally
# use 240.0.0.1 here instead of 10.0.0.1 as existing SL instances use the 240.0.0.0 network
POSTFIX_SERVER = os.environ.get("POSTFIX_SERVER", "240.0.0.1") POSTFIX_SERVER = os.environ.get("POSTFIX_SERVER", "240.0.0.1")
DISABLE_REGISTRATION = "DISABLE_REGISTRATION" in os.environ DISABLE_REGISTRATION = "DISABLE_REGISTRATION" in os.environ
# allow using a different postfix port, useful when developing locally
# Use port 587 instead of 25 when sending emails through Postfix # Use port 587 instead of 25 when sending emails through Postfix
# Useful when calling Postfix from an external network # Useful when calling Postfix from an external network
POSTFIX_SUBMISSION_TLS = "POSTFIX_SUBMISSION_TLS" in os.environ POSTFIX_SUBMISSION_TLS = "POSTFIX_SUBMISSION_TLS" in os.environ
if POSTFIX_SUBMISSION_TLS:
if "OTHER_ALIAS_DOMAINS" in os.environ: default_postfix_port = 587
OTHER_ALIAS_DOMAINS = eval(
os.environ["OTHER_ALIAS_DOMAINS"]
) # ["domain1.com", "domain2.com"]
else: else:
OTHER_ALIAS_DOMAINS = [] default_postfix_port = 25
POSTFIX_PORT = int(os.environ.get("POSTFIX_PORT", default_postfix_port))
POSTFIX_TIMEOUT = int(os.environ.get("POSTFIX_TIMEOUT", 3))
# ["domain1.com", "domain2.com"]
OTHER_ALIAS_DOMAINS = sl_getenv("OTHER_ALIAS_DOMAINS", list)
OTHER_ALIAS_DOMAINS = [d.lower().strip() for d in OTHER_ALIAS_DOMAINS]
# List of domains user can use to create alias # List of domains user can use to create alias
ALIAS_DOMAINS = OTHER_ALIAS_DOMAINS + [EMAIL_DOMAIN] if "ALIAS_DOMAINS" in os.environ:
ALIAS_DOMAINS = sl_getenv("ALIAS_DOMAINS") # ["domain1.com", "domain2.com"]
else:
ALIAS_DOMAINS = OTHER_ALIAS_DOMAINS + [EMAIL_DOMAIN]
ALIAS_DOMAINS = [d.lower().strip() for d in ALIAS_DOMAINS]
# ["domain1.com", "domain2.com"]
PREMIUM_ALIAS_DOMAINS = sl_getenv("PREMIUM_ALIAS_DOMAINS", list)
PREMIUM_ALIAS_DOMAINS = [d.lower().strip() for d in PREMIUM_ALIAS_DOMAINS]
# the alias domain used when creating the first alias for user
FIRST_ALIAS_DOMAIN = os.environ.get("FIRST_ALIAS_DOMAIN") or EMAIL_DOMAIN
# list of (priority, email server) # list of (priority, email server)
EMAIL_SERVERS_WITH_PRIORITY = eval( # e.g. [(10, "mx1.hostname."), (10, "mx2.hostname.")]
os.environ["EMAIL_SERVERS_WITH_PRIORITY"] EMAIL_SERVERS_WITH_PRIORITY = sl_getenv("EMAIL_SERVERS_WITH_PRIORITY")
) # [(10, "email.hostname.")]
# these emails are ignored when computing stats
if os.environ.get("IGNORED_EMAILS"):
IGNORED_EMAILS = eval(os.environ.get("IGNORED_EMAILS"))
else:
IGNORED_EMAILS = []
# disable the alias suffix, i.e. the ".random_word" part # disable the alias suffix, i.e. the ".random_word" part
DISABLE_ALIAS_SUFFIX = "DISABLE_ALIAS_SUFFIX" in os.environ DISABLE_ALIAS_SUFFIX = "DISABLE_ALIAS_SUFFIX" in os.environ
DKIM_PRIVATE_KEY_PATH = get_abs_path(os.environ["DKIM_PRIVATE_KEY_PATH"]) # the email address that receives all unsubscription request
DKIM_PUBLIC_KEY_PATH = get_abs_path(os.environ["DKIM_PUBLIC_KEY_PATH"]) UNSUBSCRIBER = os.environ.get("UNSUBSCRIBER")
# due to a typo, both UNSUBSCRIBER and OLD_UNSUBSCRIBER are supported
OLD_UNSUBSCRIBER = os.environ.get("OLD_UNSUBSCRIBER")
DKIM_SELECTOR = b"dkim" DKIM_SELECTOR = b"dkim"
DKIM_PRIVATE_KEY = None
with open(DKIM_PRIVATE_KEY_PATH) as f: if "DKIM_PRIVATE_KEY_PATH" in os.environ:
DKIM_PRIVATE_KEY = f.read() DKIM_PRIVATE_KEY_PATH = get_abs_path(os.environ["DKIM_PRIVATE_KEY_PATH"])
with open(DKIM_PRIVATE_KEY_PATH) as f:
DKIM_PRIVATE_KEY = f.read()
with open(DKIM_PUBLIC_KEY_PATH) as f:
DKIM_DNS_VALUE = (
f.read()
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("\r", "")
.replace("\n", "")
)
DKIM_HEADERS = [b"from", b"to", b"subject"]
# Database # Database
DB_URI = os.environ["DB_URI"] DB_URI = os.environ["DB_URI"]
DB_CONN_NAME = os.environ.get("DB_CONN_NAME", "webapp")
# Flask secret # Flask secret
FLASK_SECRET = os.environ["FLASK_SECRET"] FLASK_SECRET = os.environ["FLASK_SECRET"]
if not FLASK_SECRET:
raise RuntimeError("FLASK_SECRET is empty. Please define it.")
SESSION_COOKIE_NAME = "slapp"
MAILBOX_SECRET = FLASK_SECRET + "mailbox" MAILBOX_SECRET = FLASK_SECRET + "mailbox"
CUSTOM_ALIAS_SECRET = FLASK_SECRET + "custom_alias"
UNSUBSCRIBE_SECRET = FLASK_SECRET + "unsub"
# AWS # AWS
AWS_REGION = "eu-west-3" AWS_REGION = os.environ.get("AWS_REGION") or "eu-west-3"
BUCKET = os.environ.get("BUCKET") BUCKET = os.environ.get("BUCKET")
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
AWS_ENDPOINT_URL = os.environ.get("AWS_ENDPOINT_URL", None)
CLOUDWATCH_LOG_GROUP = CLOUDWATCH_LOG_STREAM = ""
ENABLE_CLOUDWATCH = "ENABLE_CLOUDWATCH" in os.environ
if ENABLE_CLOUDWATCH:
CLOUDWATCH_LOG_GROUP = os.environ["CLOUDWATCH_LOG_GROUP"]
CLOUDWATCH_LOG_STREAM = os.environ["CLOUDWATCH_LOG_STREAM"]
# Paddle # Paddle
try: try:
PADDLE_VENDOR_ID = int(os.environ["PADDLE_VENDOR_ID"]) PADDLE_VENDOR_ID = int(os.environ["PADDLE_VENDOR_ID"])
PADDLE_MONTHLY_PRODUCT_ID = int(os.environ["PADDLE_MONTHLY_PRODUCT_ID"]) PADDLE_MONTHLY_PRODUCT_ID = int(os.environ["PADDLE_MONTHLY_PRODUCT_ID"])
PADDLE_YEARLY_PRODUCT_ID = int(os.environ["PADDLE_YEARLY_PRODUCT_ID"]) PADDLE_YEARLY_PRODUCT_ID = int(os.environ["PADDLE_YEARLY_PRODUCT_ID"])
except: except (KeyError, ValueError):
print("Paddle param not set") print("Paddle param not set")
PADDLE_VENDOR_ID = -1 PADDLE_VENDOR_ID = -1
PADDLE_MONTHLY_PRODUCT_ID = -1 PADDLE_MONTHLY_PRODUCT_ID = -1
PADDLE_YEARLY_PRODUCT_ID = -1 PADDLE_YEARLY_PRODUCT_ID = -1
# Other Paddle product IDS
PADDLE_MONTHLY_PRODUCT_IDS = sl_getenv("PADDLE_MONTHLY_PRODUCT_IDS", list)
PADDLE_MONTHLY_PRODUCT_IDS.append(PADDLE_MONTHLY_PRODUCT_ID)
PADDLE_YEARLY_PRODUCT_IDS = sl_getenv("PADDLE_YEARLY_PRODUCT_IDS", list)
PADDLE_YEARLY_PRODUCT_IDS.append(PADDLE_YEARLY_PRODUCT_ID)
PADDLE_PUBLIC_KEY_PATH = get_abs_path( PADDLE_PUBLIC_KEY_PATH = get_abs_path(
os.environ.get("PADDLE_PUBLIC_KEY_PATH", "local_data/paddle.key.pub") os.environ.get("PADDLE_PUBLIC_KEY_PATH", "local_data/paddle.key.pub")
) )
PADDLE_AUTH_CODE = os.environ.get("PADDLE_AUTH_CODE") PADDLE_AUTH_CODE = os.environ.get("PADDLE_AUTH_CODE")
PADDLE_COUPON_ID = os.environ.get("PADDLE_COUPON_ID")
# OpenID keys, used to sign id_token # OpenID keys, used to sign id_token
OPENID_PRIVATE_KEY_PATH = get_abs_path( OPENID_PRIVATE_KEY_PATH = get_abs_path(
os.environ.get("OPENID_PRIVATE_KEY_PATH", "local_data/jwtRS256.key") os.environ.get("OPENID_PRIVATE_KEY_PATH", "local_data/jwtRS256.key")
@ -155,8 +243,10 @@ OPENID_PUBLIC_KEY_PATH = get_abs_path(
) )
# Used to generate random email # Used to generate random email
# words.txt is a list of English words and doesn't contain any "bad" word
# words_alpha.txt comes from https://github.com/dwyl/english-words and also contains bad words.
WORDS_FILE_PATH = get_abs_path( WORDS_FILE_PATH = get_abs_path(
os.environ.get("WORDS_FILE_PATH", "local_data/words_alpha.txt") os.environ.get("WORDS_FILE_PATH", "local_data/words.txt")
) )
# Used to generate random email # Used to generate random email
@ -171,17 +261,33 @@ else:
print("WARNING: Use a temp directory for GNUPGHOME", GNUPGHOME) print("WARNING: Use a temp directory for GNUPGHOME", GNUPGHOME)
# Github, Google, Facebook client id and secrets # Github, Google, Facebook, OIDC client id and secrets
GITHUB_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID") GITHUB_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID")
GITHUB_CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET") GITHUB_CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET")
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID") GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET") GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET")
FACEBOOK_CLIENT_ID = os.environ.get("FACEBOOK_CLIENT_ID") FACEBOOK_CLIENT_ID = os.environ.get("FACEBOOK_CLIENT_ID")
FACEBOOK_CLIENT_SECRET = os.environ.get("FACEBOOK_CLIENT_SECRET") FACEBOOK_CLIENT_SECRET = os.environ.get("FACEBOOK_CLIENT_SECRET")
CONNECT_WITH_OIDC_ICON = os.environ.get("CONNECT_WITH_OIDC_ICON")
OIDC_WELL_KNOWN_URL = os.environ.get("OIDC_WELL_KNOWN_URL")
OIDC_CLIENT_ID = os.environ.get("OIDC_CLIENT_ID")
OIDC_CLIENT_SECRET = os.environ.get("OIDC_CLIENT_SECRET")
OIDC_SCOPES = os.environ.get("OIDC_SCOPES")
OIDC_NAME_FIELD = os.environ.get("OIDC_NAME_FIELD", "name")
PROTON_CLIENT_ID = os.environ.get("PROTON_CLIENT_ID")
PROTON_CLIENT_SECRET = os.environ.get("PROTON_CLIENT_SECRET")
PROTON_BASE_URL = os.environ.get(
"PROTON_BASE_URL", "https://account.protonmail.com/api"
)
PROTON_VALIDATE_CERTS = "PROTON_VALIDATE_CERTS" in os.environ
CONNECT_WITH_PROTON = "CONNECT_WITH_PROTON" in os.environ
PROTON_EXTRA_HEADER_NAME = os.environ.get("PROTON_EXTRA_HEADER_NAME")
PROTON_EXTRA_HEADER_VALUE = os.environ.get("PROTON_EXTRA_HEADER_VALUE")
# in seconds # in seconds
AVATAR_URL_EXPIRATION = 3600 * 24 * 7 # 1h*24h/d*7d=1week AVATAR_URL_EXPIRATION = 3600 * 24 * 7 # 1h*24h/d*7d=1week
@ -191,9 +297,363 @@ MFA_USER_ID = "mfa_user_id"
FLASK_PROFILER_PATH = os.environ.get("FLASK_PROFILER_PATH") FLASK_PROFILER_PATH = os.environ.get("FLASK_PROFILER_PATH")
FLASK_PROFILER_PASSWORD = os.environ.get("FLASK_PROFILER_PASSWORD") FLASK_PROFILER_PASSWORD = os.environ.get("FLASK_PROFILER_PASSWORD")
# Job names # Job names
JOB_ONBOARDING_1 = "onboarding-1" JOB_ONBOARDING_1 = "onboarding-1"
JOB_ONBOARDING_2 = "onboarding-2"
JOB_ONBOARDING_3 = "onboarding-3"
JOB_ONBOARDING_4 = "onboarding-4"
JOB_BATCH_IMPORT = "batch-import"
JOB_DELETE_ACCOUNT = "delete-account"
JOB_DELETE_MAILBOX = "delete-mailbox"
JOB_DELETE_DOMAIN = "delete-domain"
JOB_SEND_USER_REPORT = "send-user-report"
JOB_SEND_PROTON_WELCOME_1 = "proton-welcome-1"
JOB_SEND_ALIAS_CREATION_EVENTS = "send-alias-creation-events"
# for pagination # for pagination
PAGE_LIMIT = 20 PAGE_LIMIT = 20
# Upload to static/upload instead of s3
LOCAL_FILE_UPLOAD = "LOCAL_FILE_UPLOAD" in os.environ
UPLOAD_DIR = None
# Rate Limiting
# nb max of activity (forward/reply) an alias can have during 1 min
MAX_ACTIVITY_DURING_MINUTE_PER_ALIAS = 10
# nb max of activity (forward/reply) a mailbox can have during 1 min
MAX_ACTIVITY_DURING_MINUTE_PER_MAILBOX = 15
if LOCAL_FILE_UPLOAD:
print("Upload files to local dir")
UPLOAD_DIR = os.path.join(ROOT_DIR, "static/upload")
if not os.path.exists(UPLOAD_DIR):
print("Create upload dir")
os.makedirs(UPLOAD_DIR)
LANDING_PAGE_URL = os.environ.get("LANDING_PAGE_URL") or "https://simplelogin.io"
STATUS_PAGE_URL = os.environ.get("STATUS_PAGE_URL") or "https://status.simplelogin.io"
# Loading PGP keys when mail_handler runs. To be used locally when init_app is not called.
LOAD_PGP_EMAIL_HANDLER = "LOAD_PGP_EMAIL_HANDLER" in os.environ
# Used when querying info on Apple API
# for iOS App
APPLE_API_SECRET = os.environ.get("APPLE_API_SECRET")
# for Mac App
MACAPP_APPLE_API_SECRET = os.environ.get("MACAPP_APPLE_API_SECRET")
# <<<<< ALERT EMAIL >>>>
# maximal number of alerts that can be sent to the same email in 24h
MAX_ALERT_24H = 4
# When a reverse-alias receives emails from un unknown mailbox
ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX = "reverse_alias_unknown_mailbox"
# When somebody is trying to spoof a reply
ALERT_DMARC_FAILED_REPLY_PHASE = "dmarc_failed_reply_phase"
# When a forwarding email is bounced
ALERT_BOUNCE_EMAIL = "bounce"
ALERT_BOUNCE_EMAIL_REPLY_PHASE = "bounce-when-reply"
# When a forwarding email is detected as spam
ALERT_SPAM_EMAIL = "spam"
# When an email is sent from a mailbox to an alias - a cycle
ALERT_SEND_EMAIL_CYCLE = "cycle"
ALERT_NON_REVERSE_ALIAS_REPLY_PHASE = "non_reverse_alias_reply_phase"
ALERT_FROM_ADDRESS_IS_REVERSE_ALIAS = "from_address_is_reverse_alias"
ALERT_TO_NOREPLY = "to_noreply"
ALERT_SPF = "spf"
ALERT_INVALID_TOTP_LOGIN = "invalid_totp_login"
# when a mailbox is also an alias
# happens when user adds a mailbox with their domain
# then later adds this domain into SimpleLogin
ALERT_MAILBOX_IS_ALIAS = "mailbox_is_alias"
AlERT_WRONG_MX_RECORD_CUSTOM_DOMAIN = "custom_domain_mx_record_issue"
# alert when a new alias is about to be created on a disabled directory
ALERT_DIRECTORY_DISABLED_ALIAS_CREATION = "alert_directory_disabled_alias_creation"
ALERT_COMPLAINT_REPLY_PHASE = "alert_complaint_reply_phase"
ALERT_COMPLAINT_FORWARD_PHASE = "alert_complaint_forward_phase"
ALERT_COMPLAINT_TRANSACTIONAL_PHASE = "alert_complaint_transactional_phase"
ALERT_QUARANTINE_DMARC = "alert_quarantine_dmarc"
ALERT_DUAL_SUBSCRIPTION_WITH_PARTNER = "alert_dual_sub_with_partner"
ALERT_WARN_MULTIPLE_SUBSCRIPTIONS = "alert_multiple_subscription"
# <<<<< END ALERT EMAIL >>>>
# Disable onboarding emails
DISABLE_ONBOARDING = "DISABLE_ONBOARDING" in os.environ
HCAPTCHA_SECRET = os.environ.get("HCAPTCHA_SECRET")
HCAPTCHA_SITEKEY = os.environ.get("HCAPTCHA_SITEKEY")
PLAUSIBLE_HOST = os.environ.get("PLAUSIBLE_HOST")
PLAUSIBLE_DOMAIN = os.environ.get("PLAUSIBLE_DOMAIN")
# server host
HOST = socket.gethostname()
SPAMASSASSIN_HOST = os.environ.get("SPAMASSASSIN_HOST")
# by default use a tolerant score
if "MAX_SPAM_SCORE" in os.environ:
MAX_SPAM_SCORE = float(os.environ["MAX_SPAM_SCORE"])
else:
MAX_SPAM_SCORE = 5.5
# use a more restrictive score when replying
if "MAX_REPLY_PHASE_SPAM_SCORE" in os.environ:
MAX_REPLY_PHASE_SPAM_SCORE = float(os.environ["MAX_REPLY_PHASE_SPAM_SCORE"])
else:
MAX_REPLY_PHASE_SPAM_SCORE = 5
PGP_SENDER_PRIVATE_KEY = None
PGP_SENDER_PRIVATE_KEY_PATH = os.environ.get("PGP_SENDER_PRIVATE_KEY_PATH")
if PGP_SENDER_PRIVATE_KEY_PATH:
with open(get_abs_path(PGP_SENDER_PRIVATE_KEY_PATH)) as f:
PGP_SENDER_PRIVATE_KEY = f.read()
# the signer address that signs outgoing encrypted emails
PGP_SIGNER = os.environ.get("PGP_SIGNER")
# emails that have empty From address is sent from this special reverse-alias
NOREPLY = os.environ.get("NOREPLY", f"noreply@{EMAIL_DOMAIN}")
# list of no reply addresses
NOREPLIES = sl_getenv("NOREPLIES", list) or [NOREPLY]
COINBASE_WEBHOOK_SECRET = os.environ.get("COINBASE_WEBHOOK_SECRET")
COINBASE_CHECKOUT_ID = os.environ.get("COINBASE_CHECKOUT_ID")
COINBASE_API_KEY = os.environ.get("COINBASE_API_KEY")
try:
COINBASE_YEARLY_PRICE = float(os.environ["COINBASE_YEARLY_PRICE"])
except Exception:
COINBASE_YEARLY_PRICE = 30.00
ALIAS_LIMIT = os.environ.get("ALIAS_LIMIT") or "100/day;50/hour;5/minute"
ENABLE_SPAM_ASSASSIN = "ENABLE_SPAM_ASSASSIN" in os.environ
ALIAS_RANDOM_SUFFIX_LENGTH = int(os.environ.get("ALIAS_RAND_SUFFIX_LENGTH", 5))
try:
HIBP_SCAN_INTERVAL_DAYS = int(os.environ.get("HIBP_SCAN_INTERVAL_DAYS"))
except Exception:
HIBP_SCAN_INTERVAL_DAYS = 7
HIBP_API_KEYS = sl_getenv("HIBP_API_KEYS", list) or []
HIBP_MAX_ALIAS_CHECK = 10_000
HIBP_RPM = int(os.environ.get("HIBP_API_RPM", 100))
HIBP_SKIP_PARTNER_ALIAS = os.environ.get("HIBP_SKIP_PARTNER_ALIAS")
KEEP_OLD_DATA_DAYS = 30
POSTMASTER = os.environ.get("POSTMASTER")
# store temporary files, especially for debugging
TEMP_DIR = os.environ.get("TEMP_DIR")
# Store unsent emails
SAVE_UNSENT_DIR = os.environ.get("SAVE_UNSENT_DIR")
if SAVE_UNSENT_DIR and not os.path.isdir(SAVE_UNSENT_DIR):
try:
os.makedirs(SAVE_UNSENT_DIR)
except FileExistsError:
pass
# enable the alias automation disable: an alias can be automatically disabled if it has too many bounces
ALIAS_AUTOMATIC_DISABLE = "ALIAS_AUTOMATIC_DISABLE" in os.environ
# whether the DKIM signing is handled by Rspamd
RSPAMD_SIGN_DKIM = "RSPAMD_SIGN_DKIM" in os.environ
TWILIO_AUTH_TOKEN = os.environ.get("TWILIO_AUTH_TOKEN")
PHONE_PROVIDER_1_HEADER = "X-SimpleLogin-Secret"
PHONE_PROVIDER_1_SECRET = os.environ.get("PHONE_PROVIDER_1_SECRET")
PHONE_PROVIDER_2_HEADER = os.environ.get("PHONE_PROVIDER_2_HEADER")
PHONE_PROVIDER_2_SECRET = os.environ.get("PHONE_PROVIDER_2_SECRET")
ZENDESK_HOST = os.environ.get("ZENDESK_HOST")
ZENDESK_API_TOKEN = os.environ.get("ZENDESK_API_TOKEN")
ZENDESK_ENABLED = "ZENDESK_ENABLED" in os.environ
DMARC_CHECK_ENABLED = "DMARC_CHECK_ENABLED" in os.environ
# Bounces can happen after 5 days
VERP_MESSAGE_LIFETIME = 5 * 86400
VERP_PREFIX = os.environ.get("VERP_PREFIX") or "sl"
# Generate with python3 -c 'import secrets; print(secrets.token_hex(28))'
VERP_EMAIL_SECRET = os.environ.get("VERP_EMAIL_SECRET") or (
FLASK_SECRET + "pleasegenerateagoodrandomtoken"
)
if len(VERP_EMAIL_SECRET) < 32:
raise RuntimeError(
"Please, set VERP_EMAIL_SECRET to a random string at least 32 chars long"
)
ALIAS_TRANSFER_TOKEN_SECRET = os.environ.get("ALIAS_TRANSFER_TOKEN_SECRET") or (
FLASK_SECRET + "aliastransfertoken"
)
def get_allowed_redirect_domains() -> List[str]:
allowed_domains = sl_getenv("ALLOWED_REDIRECT_DOMAINS", list)
if allowed_domains:
return allowed_domains
parsed_url = urlparse(URL)
return [parsed_url.hostname]
ALLOWED_REDIRECT_DOMAINS = get_allowed_redirect_domains()
def setup_nameservers():
nameservers = os.environ.get("NAMESERVERS", "1.1.1.1")
return nameservers.split(",")
NAMESERVERS = setup_nameservers()
DISABLE_CREATE_CONTACTS_FOR_FREE_USERS = os.environ.get(
"DISABLE_CREATE_CONTACTS_FOR_FREE_USERS", False
)
# Expect format hits,seconds:hits,seconds...
# Example 1,10:4,60 means 1 in the last 10 secs or 4 in the last 60 secs
def getRateLimitFromConfig(
env_var: string, default: string = ""
) -> list[tuple[int, int]]:
value = os.environ.get(env_var, default)
if not value:
return []
entries = [entry for entry in value.split(":")]
limits = []
for entry in entries:
fields = entry.split(",")
limit = (int(fields[0]), int(fields[1]))
limits.append(limit)
return limits
ALIAS_CREATE_RATE_LIMIT_FREE = getRateLimitFromConfig(
"ALIAS_CREATE_RATE_LIMIT_FREE", "10,900:50,3600"
)
ALIAS_CREATE_RATE_LIMIT_PAID = getRateLimitFromConfig(
"ALIAS_CREATE_RATE_LIMIT_PAID", "50,900:200,3600"
)
PARTNER_API_TOKEN_SECRET = os.environ.get("PARTNER_API_TOKEN_SECRET") or (
FLASK_SECRET + "partnerapitoken"
)
JOB_MAX_ATTEMPTS = 5
JOB_TAKEN_RETRY_WAIT_MINS = 30
# MEM_STORE
MEM_STORE_URI = os.environ.get("MEM_STORE_URI", None)
# Recovery codes hash salt
RECOVERY_CODE_HMAC_SECRET = os.environ.get("RECOVERY_CODE_HMAC_SECRET") or (
FLASK_SECRET + "generatearandomtoken"
)
if not RECOVERY_CODE_HMAC_SECRET or len(RECOVERY_CODE_HMAC_SECRET) < 16:
raise RuntimeError(
"Please define RECOVERY_CODE_HMAC_SECRET in your configuration with a random string at least 16 chars long"
)
# the minimum rspamd spam score above which emails that fail DMARC should be quarantined
if "MIN_RSPAMD_SCORE_FOR_FAILED_DMARC" in os.environ:
MIN_RSPAMD_SCORE_FOR_FAILED_DMARC = float(
os.environ["MIN_RSPAMD_SCORE_FOR_FAILED_DMARC"]
)
else:
MIN_RSPAMD_SCORE_FOR_FAILED_DMARC = None
# run over all reverse alias for an alias and replace them with sender address
ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT = (
"ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT" in os.environ
)
if ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT:
# max number of reverse alias that can be replaced
MAX_NB_REVERSE_ALIAS_REPLACEMENT = int(
os.environ["MAX_NB_REVERSE_ALIAS_REPLACEMENT"]
)
# Only used for tests
SKIP_MX_LOOKUP_ON_CHECK = False
DISABLE_RATE_LIMIT = "DISABLE_RATE_LIMIT" in os.environ
SUBSCRIPTION_CHANGE_WEBHOOK = os.environ.get("SUBSCRIPTION_CHANGE_WEBHOOK", None)
MAX_API_KEYS = int(os.environ.get("MAX_API_KEYS", 30))
UPCLOUD_USERNAME = os.environ.get("UPCLOUD_USERNAME", None)
UPCLOUD_PASSWORD = os.environ.get("UPCLOUD_PASSWORD", None)
UPCLOUD_DB_ID = os.environ.get("UPCLOUD_DB_ID", None)
STORE_TRANSACTIONAL_EMAILS = "STORE_TRANSACTIONAL_EMAILS" in os.environ
EVENT_WEBHOOK = os.environ.get("EVENT_WEBHOOK", None)
# We want it disabled by default, so only skip if defined
EVENT_WEBHOOK_SKIP_VERIFY_SSL = "EVENT_WEBHOOK_SKIP_VERIFY_SSL" in os.environ
EVENT_WEBHOOK_DISABLE = "EVENT_WEBHOOK_DISABLE" in os.environ
def read_webhook_enabled_user_ids() -> Optional[List[int]]:
user_ids = os.environ.get("EVENT_WEBHOOK_ENABLED_USER_IDS", None)
if user_ids is None:
return None
ids = []
for user_id in user_ids.split(","):
try:
ids.append(int(user_id.strip()))
except ValueError:
pass
return ids
EVENT_WEBHOOK_ENABLED_USER_IDS: Optional[List[int]] = read_webhook_enabled_user_ids()
# Allow to define a different DB_URI for the event listener, in case we want to skip the connection pool
# It defaults to the regular DB_URI in case it's needed
EVENT_LISTENER_DB_URI = os.environ.get("EVENT_LISTENER_DB_URI", DB_URI)
def read_partner_dict(var: str) -> dict[int, str]:
partner_value = get_env_dict(var)
if len(partner_value) == 0:
return {}
res: dict[int, str] = {}
for partner_id in partner_value.keys():
try:
partner_id_int = int(partner_id.strip())
res[partner_id_int] = partner_value[partner_id]
except ValueError:
pass
return res
PARTNER_DOMAINS: dict[int, str] = read_partner_dict("PARTNER_DOMAINS")
PARTNER_DOMAIN_VALIDATION_PREFIXES: dict[int, str] = read_partner_dict(
"PARTNER_DOMAIN_VALIDATION_PREFIXES"
)

2
app/constants.py Normal file
View File

@ -0,0 +1,2 @@
HEADER_ALLOW_API_COOKIES = "X-Sl-Allowcookies"
DMARC_RECORD = "v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s"

89
app/contact_utils.py Normal file
View File

@ -0,0 +1,89 @@
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from sqlalchemy.exc import IntegrityError
from app.db import Session
from app.email_utils import generate_reply_email
from app.email_validation import is_valid_email
from app.log import LOG
from app.models import Contact, Alias
from app.utils import sanitize_email
class ContactCreateError(Enum):
InvalidEmail = "Invalid email"
@dataclass
class ContactCreateResult:
contact: Optional[Contact]
error: Optional[ContactCreateError]
def __update_contact_if_needed(
contact: Contact, name: Optional[str], mail_from: Optional[str]
) -> ContactCreateResult:
if name and contact.name != name:
LOG.d(f"Setting {contact} name to {name}")
contact.name = name
Session.commit()
if mail_from and contact.mail_from is None:
LOG.d(f"Setting {contact} mail_from to {mail_from}")
contact.mail_from = mail_from
Session.commit()
return ContactCreateResult(contact, None)
def create_contact(
email: str,
name: Optional[str],
alias: Alias,
mail_from: Optional[str] = None,
allow_empty_email: bool = False,
automatic_created: bool = False,
from_partner: bool = False,
) -> ContactCreateResult:
if name is not None:
name = name[: Contact.MAX_NAME_LENGTH]
if name is not None and "\x00" in name:
LOG.w("Cannot use contact name because has \\x00")
name = ""
if not is_valid_email(email):
LOG.w(f"invalid contact email {email}")
if not allow_empty_email:
return ContactCreateResult(None, ContactCreateError.InvalidEmail)
LOG.d("Create a contact with invalid email for %s", alias)
# either reuse a contact with empty email or create a new contact with empty email
email = ""
email = sanitize_email(email, not_lower=True)
contact = Contact.get_by(alias_id=alias.id, website_email=email)
if contact is not None:
return __update_contact_if_needed(contact, name, mail_from)
reply_email = generate_reply_email(email, alias)
try:
flags = Contact.FLAG_PARTNER_CREATED if from_partner else 0
contact = Contact.create(
user_id=alias.user_id,
alias_id=alias.id,
website_email=email,
name=name,
reply_email=reply_email,
mail_from=mail_from,
automatic_created=automatic_created,
flags=flags,
invalid_email=email == "",
commit=True,
)
LOG.d(
f"Created contact {contact} for alias {alias} with email {email} invalid_email={contact.invalid_email}"
)
except IntegrityError:
Session.rollback()
LOG.info(
f"Contact with email {email} for alias_id {alias.id} already existed, fetching from DB"
)
contact = Contact.get_by(alias_id=alias.id, website_email=email)
return __update_contact_if_needed(contact, name, mail_from)
return ContactCreateResult(contact, None)

142
app/custom_domain_utils.py Normal file
View File

@ -0,0 +1,142 @@
import arrow
import re
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from app.config import JOB_DELETE_DOMAIN
from app.db import Session
from app.email_utils import get_email_domain_part
from app.log import LOG
from app.models import User, CustomDomain, SLDomain, Mailbox, Job
_ALLOWED_DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$")
@dataclass
class CreateCustomDomainResult:
message: str = ""
message_category: str = ""
success: bool = False
instance: Optional[CustomDomain] = None
redirect: Optional[str] = None
class CannotUseDomainReason(Enum):
InvalidDomain = 1
BuiltinDomain = 2
DomainAlreadyUsed = 3
DomainPartOfUserEmail = 4
DomainUserInMailbox = 5
def message(self, domain: str) -> str:
if self == CannotUseDomainReason.InvalidDomain:
return "This is not a valid domain"
elif self == CannotUseDomainReason.BuiltinDomain:
return "A custom domain cannot be a built-in domain."
elif self == CannotUseDomainReason.DomainAlreadyUsed:
return f"{domain} already used"
elif self == CannotUseDomainReason.DomainPartOfUserEmail:
return "You cannot add a domain that you are currently using for your personal email. Please change your personal email to your real email"
elif self == CannotUseDomainReason.DomainUserInMailbox:
return f"{domain} already used in a SimpleLogin mailbox"
else:
raise Exception("Invalid CannotUseDomainReason")
def is_valid_domain(domain: str) -> bool:
"""
Checks that a domain is valid according to RFC 1035
"""
if len(domain) > 255:
return False
if domain.endswith("."):
domain = domain[:-1] # Strip the trailing dot
labels = domain.split(".")
if not labels:
return False
for label in labels:
if not _ALLOWED_DOMAIN_REGEX.match(label):
return False
return True
def sanitize_domain(domain: str) -> str:
new_domain = domain.lower().strip()
if new_domain.startswith("http://"):
new_domain = new_domain[len("http://") :]
if new_domain.startswith("https://"):
new_domain = new_domain[len("https://") :]
return new_domain
def can_domain_be_used(user: User, domain: str) -> Optional[CannotUseDomainReason]:
if not is_valid_domain(domain):
return CannotUseDomainReason.InvalidDomain
elif SLDomain.get_by(domain=domain):
return CannotUseDomainReason.BuiltinDomain
elif CustomDomain.get_by(domain=domain):
return CannotUseDomainReason.DomainAlreadyUsed
elif get_email_domain_part(user.email) == domain:
return CannotUseDomainReason.DomainPartOfUserEmail
elif Mailbox.filter(
Mailbox.verified.is_(True), Mailbox.email.endswith(f"@{domain}")
).first():
return CannotUseDomainReason.DomainUserInMailbox
else:
return None
def create_custom_domain(
user: User, domain: str, partner_id: Optional[int] = None
) -> CreateCustomDomainResult:
if not user.is_premium():
return CreateCustomDomainResult(
message="Only premium plan can add custom domain",
message_category="warning",
)
new_domain = sanitize_domain(domain)
domain_forbidden_cause = can_domain_be_used(user, new_domain)
if domain_forbidden_cause:
return CreateCustomDomainResult(
message=domain_forbidden_cause.message(new_domain), message_category="error"
)
new_custom_domain = CustomDomain.create(domain=new_domain, user_id=user.id)
# new domain has ownership verified if its parent has the ownership verified
for root_cd in user.custom_domains:
if new_domain.endswith("." + root_cd.domain) and root_cd.ownership_verified:
LOG.i(
"%s ownership verified thanks to %s",
new_custom_domain,
root_cd,
)
new_custom_domain.ownership_verified = True
# Add the partner_id in case it's passed
if partner_id is not None:
new_custom_domain.partner_id = partner_id
Session.commit()
return CreateCustomDomainResult(
success=True,
instance=new_custom_domain,
)
def delete_custom_domain(domain: CustomDomain):
# Schedule delete domain job
LOG.w("schedule delete domain job for %s", domain)
domain.pending_deletion = True
Job.create(
name=JOB_DELETE_DOMAIN,
payload={"custom_domain_id": domain.id},
run_at=arrow.now(),
commit=True,
)

View File

@ -0,0 +1,157 @@
from dataclasses import dataclass
from typing import Optional
from app import config
from app.constants import DMARC_RECORD
from app.db import Session
from app.dns_utils import (
DNSClient,
is_mx_equivalent,
get_network_dns_client,
)
from app.models import CustomDomain
@dataclass
class DomainValidationResult:
success: bool
errors: [str]
class CustomDomainValidation:
def __init__(
self,
dkim_domain: str,
dns_client: DNSClient = get_network_dns_client(),
partner_domains: Optional[dict[int, str]] = None,
partner_domains_validation_prefixes: Optional[dict[int, str]] = None,
):
self.dkim_domain = dkim_domain
self._dns_client = dns_client
self._partner_domains = partner_domains or config.PARTNER_DOMAINS
self._partner_domain_validation_prefixes = (
partner_domains_validation_prefixes
or config.PARTNER_DOMAIN_VALIDATION_PREFIXES
)
def get_ownership_verification_record(self, domain: CustomDomain) -> str:
prefix = "sl"
if (
domain.partner_id is not None
and domain.partner_id in self._partner_domain_validation_prefixes
):
prefix = self._partner_domain_validation_prefixes[domain.partner_id]
return f"{prefix}-verification={domain.ownership_txt_token}"
def get_dkim_records(self, domain: CustomDomain) -> {str: str}:
"""
Get a list of dkim records to set up. Depending on the custom_domain, whether if it's from a partner or not,
it will return the default ones or the partner ones.
"""
# By default use the default domain
dkim_domain = self.dkim_domain
if domain.partner_id is not None:
# Domain is from a partner. Retrieve the partner config and use that domain if exists
dkim_domain = self._partner_domains.get(domain.partner_id, dkim_domain)
return {
f"{key}._domainkey": f"{key}._domainkey.{dkim_domain}"
for key in ("dkim", "dkim02", "dkim03")
}
def validate_dkim_records(self, custom_domain: CustomDomain) -> dict[str, str]:
"""
Check if dkim records are properly set for this custom domain.
Returns empty list if all records are ok. Other-wise return the records that aren't properly configured
"""
correct_records = {}
invalid_records = {}
expected_records = self.get_dkim_records(custom_domain)
for prefix, expected_record in expected_records.items():
custom_record = f"{prefix}.{custom_domain.domain}"
dkim_record = self._dns_client.get_cname_record(custom_record)
if dkim_record == expected_record:
correct_records[prefix] = custom_record
else:
invalid_records[custom_record] = dkim_record or "empty"
# HACK
# As initially we only had one dkim record, we want to allow users that had only the original dkim record and
# the domain validated to continue seeing it as validated (although showing them the missing records).
# However, if not even the original dkim record is right, even if the domain was dkim_verified in the past,
# we will remove the dkim_verified flag.
# This is done in order to give users with the old dkim config (only one) to update their CNAMEs
if custom_domain.dkim_verified:
# Check if at least the original dkim is there
if correct_records.get("dkim._domainkey") is not None:
# Original dkim record is there. Return the missing records (if any) and don't clear the flag
return invalid_records
# Original DKIM record is not there, which means the DKIM config is not finished. Proceed with the
# rest of the code path, returning the invalid records and clearing the flag
custom_domain.dkim_verified = len(invalid_records) == 0
Session.commit()
return invalid_records
def validate_domain_ownership(
self, custom_domain: CustomDomain
) -> DomainValidationResult:
"""
Check if the custom_domain has added the ownership verification records
"""
txt_records = self._dns_client.get_txt_record(custom_domain.domain)
expected_verification_record = self.get_ownership_verification_record(
custom_domain
)
if expected_verification_record in txt_records:
custom_domain.ownership_verified = True
Session.commit()
return DomainValidationResult(success=True, errors=[])
else:
return DomainValidationResult(success=False, errors=txt_records)
def validate_mx_records(
self, custom_domain: CustomDomain
) -> DomainValidationResult:
mx_domains = self._dns_client.get_mx_domains(custom_domain.domain)
if not is_mx_equivalent(mx_domains, config.EMAIL_SERVERS_WITH_PRIORITY):
return DomainValidationResult(
success=False,
errors=[f"{priority} {domain}" for (priority, domain) in mx_domains],
)
else:
custom_domain.verified = True
Session.commit()
return DomainValidationResult(success=True, errors=[])
def validate_spf_records(
self, custom_domain: CustomDomain
) -> DomainValidationResult:
spf_domains = self._dns_client.get_spf_domain(custom_domain.domain)
if config.EMAIL_DOMAIN in spf_domains:
custom_domain.spf_verified = True
Session.commit()
return DomainValidationResult(success=True, errors=[])
else:
custom_domain.spf_verified = False
Session.commit()
return DomainValidationResult(
success=False,
errors=self._dns_client.get_txt_record(custom_domain.domain),
)
def validate_dmarc_records(
self, custom_domain: CustomDomain
) -> DomainValidationResult:
txt_records = self._dns_client.get_txt_record("_dmarc." + custom_domain.domain)
if DMARC_RECORD in txt_records:
custom_domain.dmarc_verified = True
Session.commit()
return DomainValidationResult(success=True, errors=[])
else:
custom_domain.dmarc_verified = False
Session.commit()
return DomainValidationResult(success=False, errors=txt_records)

View File

@ -3,18 +3,71 @@ from .views import (
pricing, pricing,
setting, setting,
custom_alias, custom_alias,
subdomain,
billing, billing,
alias_log, alias_log,
alias_export,
unsubscribe, unsubscribe,
api_key, api_key,
custom_domain, custom_domain,
alias_contact_manager, alias_contact_manager,
enter_sudo,
mfa_setup, mfa_setup,
mfa_cancel, mfa_cancel,
fido_setup,
coupon,
fido_manage,
domain_detail, domain_detail,
lifetime_licence, lifetime_licence,
directory, directory,
mailbox, mailbox,
deleted_alias,
mailbox_detail, mailbox_detail,
refused_email,
referral,
contact_detail,
setup_done,
batch_import,
alias_transfer,
app,
delete_account,
notification,
support,
account_setting,
) )
__all__ = [
"index",
"pricing",
"setting",
"custom_alias",
"subdomain",
"billing",
"alias_log",
"alias_export",
"unsubscribe",
"api_key",
"custom_domain",
"alias_contact_manager",
"enter_sudo",
"mfa_setup",
"mfa_cancel",
"fido_setup",
"coupon",
"fido_manage",
"domain_detail",
"lifetime_licence",
"directory",
"mailbox",
"mailbox_detail",
"refused_email",
"referral",
"contact_detail",
"setup_done",
"batch_import",
"alias_transfer",
"app",
"delete_account",
"notification",
"support",
"account_setting",
]

View File

@ -1,114 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "dashboard" %}
{% block title %}
Alias Contact Manager
{% endblock %}
{% block default_content %}
<div class="page-header row">
<h3 class="page-title col">
{{ alias }}
</h3>
</div>
<div class="alert alert-primary" role="alert">
<p>
To send an email from your alias to someone, says <b>friend@example.com</b>, you need to: <br>
1. Create a special email address called <em>reverse-alias</em> for friend@example.com using the form below <br>
2. Send the email to the reverse-alias <em>instead of</em> friend@example.com
<br>
3. SimpleLogin will send this email from the alias to friend@example.com for you
</p>
<p>
This might sound complicated but trust us, only the first time is a bit awkward.
</p>
<p>
{% if gen_email.mailbox_id %}
Make sure you send the email from the mailbox <b>{{ gen_email.mailbox.email }}</b>.
This is because only the mailbox that owns the alias can send emails from it.
{% else %}
Make sure you send the email from your personal email address ({{ current_user.email }}).
{% endif %}
</p>
</div>
<form method="post">
<input type="hidden" name="form-name" value="create">
{{ new_contact_form.csrf_token }}
<label class="form-label">Where do you want to send email to?</label>
{{ new_contact_form.email(class="form-control", placeholder="First Last <email@example.com>") }}
{{ render_field_errors(new_contact_form.email) }}
<button class="btn btn-primary mt-2">Create reverse-alias</button>
</form>
<div class="row">
{% for forward_email in forward_emails %}
<div class="col-md-6">
<div class="my-2 p-2 card {% if forward_email.id == forward_email_id %} highlight-row {% endif %}">
<div>
<span>
<a href="{{ 'mailto:' + forward_email.website_send_to() }}"
data-toggle="tooltip"
title="You can click on this to open your email client. Or use the copy button 👉"
class="font-weight-bold">*************************</a>
<span class="clipboard btn btn-sm btn-success copy-btn" data-toggle="tooltip"
title="Copy to clipboard"
data-clipboard-text="{{ forward_email.website_send_to() }}">
Copy reverse-alias
</span>
</span>
</div>
<div>
<i class="fe fe-mail"></i> ➡ {{ forward_email.website_from or forward_email.website_email }}
</div>
<div class="mb-2 text-muted small-text">
Created {{ forward_email.created_at | dt }} <br>
{% if forward_email.last_reply() %}
{% set email_log = forward_email.last_reply() %}
Last email sent {{ email_log.created_at | dt }}
{% endif %}
</div>
<div>
<form method="post">
<input type="hidden" name="form-name" value="delete">
<input type="hidden" name="forward-email-id" value="{{ forward_email.id }}">
<span class="card-link btn btn-link float-right delete-forward-email">
Delete
</span>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-forward-email").on("click", function (e) {
notie.confirm({
text: "Activity history associated with this reverse-alias will be deleted, " +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -1,157 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "dashboard" %}
{% block head %}
<style>
{# https://bootsnipp.com/snippets/rljEW#}
.card-counter {
box-shadow: 2px 2px 10px #DADADA;
margin: 5px;
padding: 20px 10px;
background-color: #fff;
height: 100px;
border-radius: 5px;
transition: .3s linear all;
}
.card-counter:hover {
box-shadow: 4px 4px 20px #DADADA;
transition: .3s linear all;
}
.card-counter.primary {
background-color: #007bff;
color: #FFF;
}
.card-counter.danger {
background-color: #ef5350;
color: #FFF;
}
.card-counter.success {
background-color: #66bb6a;
color: #FFF;
}
.card-counter.info {
background-color: #26c6da;
color: #FFF;
}
.card-counter i {
font-size: 5em;
opacity: 0.2;
}
.card-counter .count-numbers {
position: absolute;
right: 35px;
top: 20px;
font-size: 32px;
display: block;
}
.card-counter .count-name {
position: absolute;
right: 35px;
top: 65px;
text-transform: capitalize;
opacity: 0.5;
display: block;
font-size: 18px;
}
</style>
{% endblock %}
{% block title %}
Alias Activity
{% endblock %}
{% block default_content %}
<div class="page-header row ml-0">
<h3 class="page-title col">
{{ alias }}
</h3>
</div>
<div class="row">
<div class="col-md-3 col-sm-6">
<div class="card-counter primary">
<i class="fa fa-at"></i>
<span class="count-numbers">{{ total }}</span>
<span class="count-name">Email Handled</span>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="card-counter primary">
<i class="fa fa-paper-plane"></i>
<span class="count-numbers">{{ email_forwarded }}</span>
<span class="count-name">Email Forwarded</span>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="card-counter primary">
<i class="fa fa-reply"></i>
<span class="count-numbers">{{ email_replied }}</span>
<span class="count-name">Email Replied</span>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="card-counter danger">
<i class="fa fa-ban"></i>
<span class="count-numbers">{{ email_blocked }}</span>
<span class="count-name">Email Blocked</span>
</div>
</div>
</div>
<h2 class="pt-4">Activities</h2>
<div class="row">
{% for log in logs %}
<div class="col-12">
<div class="my-2 p-2 card border-light">
<div class="font-weight-bold">{{ log.when | dt }}
{% if log.bounced %} ⚠️ {% endif %}
</div>
<div>
{% if log.bounced %}
<span class="mr-2">{{ log.website_from or log.website_email }}</span>
<img src="{{ url_for('static', filename='arrows/forward-arrow.svg') }}" class="arrow">
<span class="ml-2">{{ log.alias }}</span>
<img src="{{ url_for('static', filename='arrows/blocked-arrow.svg') }}" class="arrow">
<span class="ml-2">{{ log.mailbox }}</span>
{% else %}
<span class="mr-2">{{ log.website_from or log.website_email }}</span>
<span>
{% if log.is_reply %}
<img src="{{ url_for('static', filename='arrows/reply-arrow.svg') }}" class="arrow">
{% elif log.blocked %}
<img src="{{ url_for('static', filename='arrows/blocked-arrow.svg') }}" class="arrow">
{% else %}
<img src="{{ url_for('static', filename='arrows/forward-arrow.svg') }}" class="arrow">
{% endif %}
</span>
<span class="ml-2">{{ log.alias }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
<nav aria-label="Alias log navigation">
<ul class="pagination">
<li class="page-item {% if page_id == 0 %}disabled{% endif %}">
<a class="page-link"
href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id-1) }}">Previous</a>
</li>
<li class="page-item {% if last_page %}disabled{% endif %}">
<a class="page-link" href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id+1) }}">Next</a>
</li>
</ul>
</nav>
{% endblock %}
{% block script %}
{% endblock %}

View File

@ -1,137 +0,0 @@
{% extends 'default.html' %}
{% block title %}
API Key
{% endblock %}
{% set active_page = "api_key" %}
{% block head %}
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="h3"> API Key </h1>
<div class="alert alert-primary" role="alert">
The API Key is used on the SimpleLogin Chrome/Firefox/Safari extension. <br>
You can install the Chrome extension on
<a href="https://chrome.google.com/webstore/detail/simplelogin-extension/dphilobhebphkdjbpfohgikllaljmgbn"
target="_blank">Chrome Store<i class="fe fe-external-link"></i></a>,
Firefox add-on on <a href="https://addons.mozilla.org/en-GB/firefox/addon/simplelogin/"
target="_blank">Firefox<i
class="fe fe-external-link"></i></a>
and Safari extension on <a
href="https://apps.apple.com/us/app/simplelogin/id1494051017?mt=12&fbclid=IwAR0M0nnEKgoieMkmx91TSXrtcScj7GouqRxGgXeJz2un_5ydhIKlbAI79Io"
target="_blank">AppStore<i class="fe fe-external-link"></i></a>
<br>
Please copy and paste the API key below into the extension to get started. <br>
<span class="text-danger">
Your API Keys are secret and should be treated as passwords.
</span>
</div>
{% for api_key in api_keys %}
<div class="card" style="max-width: 50rem">
<div class="card-body">
<h5 class="card-title">{{ api_key.name }}</h5>
<h6 class="card-subtitle mb-2 text-muted">
{% if api_key.last_used %}
Last used: {{ api_key.last_used | dt }} <br>
Used: {{ api_key.times }} times.
{% else %}
Never used
{% endif %}
</h6>
<div class="input-group">
<input class="form-control" id="apikey-{{ api_key.id }}" readonly value="**********">
<div class="input-group-append">
<span class="input-group-text">
<i class="fe fe-eye toggle-api-key" data-show="off" data-secret="{{ api_key.code }}"
></i>
</span>
</div>
</div>
<br>
<div class="row">
<div class="col">
<button class="clipboard btn btn-primary" data-clipboard-action="copy"
data-clipboard-text="{{ api_key.code }}"
data-clipboard-target="#apikey-{{ api_key.id }}">
Copy &nbsp; &nbsp; <i class="fe fe-clipboard"></i>
</button>
</div>
<div class="col">
<form method="post">
<input type="hidden" name="form-name" value="delete">
<input type="hidden" name="api-key-id" value="{{ api_key.id }}">
<span class="card-link btn btn-link float-right delete-api-key">
Delete
</span>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
<hr>
<form method="post">
{{ new_api_key_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<label class="form-label">Api Key Name</label>
<small>Name of the api key, e.g. where it will be used.</small>
{{ new_api_key_form.name(class="form-control", placeholder="Chrome, Firefox") }}
{{ render_field_errors(new_api_key_form.name) }}
<button class="btn btn-lg btn-success mt-2">Create</button>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-api-key").on("click", function (e) {
notie.confirm({
text: "If this api key is currently in use, you need to replace it with another api key, " +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
$(".toggle-api-key").on('click', function (event) {
let that = $(this);
let apiInput = that.parent().parent().parent().find("input");
if (that.attr("data-show") === "off") {
let apiKey = $(this).attr("data-secret");
apiInput.val(apiKey);
that.addClass("fe-eye-off");
that.removeClass("fe-eye");
that.attr("data-show", "on");
} else {
that.removeClass("fe-eye-off");
that.addClass("fe-eye");
apiInput.val("**********");
that.attr("data-show", "off");
}
});
</script>
{% endblock %}

View File

@ -1,79 +0,0 @@
{% extends 'default.html' %}
{% block title %}
Billing
{% endblock %}
{% block head %}
{% endblock %}
{% block default_content %}
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
<h1 class="h3 mb-5"> Billing </h1>
{% if sub.cancelled %}
<p>
You are on the <b>{{ sub.plan_name() }}</b> plan. <br>
You have canceled your subscription and it will end on {{current_user.next_bill_date()}}
({{ sub.next_bill_date | dt }}).
</p>
<hr>
<p>
If you change your mind you can subscribe again to SimpleLogin but please note that this will be a completely
new subscription and
your payment method will be charged <b>immediately</b>.
<br>
We are going to send you an email by the end of the subscription so maybe you can upgrade at that time.
<br>
<a href="{{ url_for('dashboard.pricing') }}" class="btn btn-primary mt-2">Re-subscribe</a>
</p>
{% else %}
<p>
You are on the <b>{{ sub.plan_name() }}</b> plan. Thank you very much for supporting
SimpleLogin. 🙌
</p>
<div class="mt-3">
Click here to update billing information on Paddle, our payment partner: <br>
<a class="btn btn-success" href="{{ sub.update_url }}"> Update billing information </a>
</div>
<hr>
<div>
Don't want to protect your inbox anymore? <br>
<form method="post">
<input type="hidden" name="form-name" value="cancel">
<span class="cancel btn btn-warning">
Cancel subscription <i class="fe fe-alert-triangle text-danger"></i>
</span>
</form>
</div>
{% endif %}
</div>
{% endblock %}
{% block script %}
<script>
$(".cancel").on("click", function (e) {
notie.confirm({
text: `This operation is irreversible, please confirm`,
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -1,91 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "dashboard" %}
{% block title %}
Custom Alias
{% endblock %}
{% block default_content %}
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
<h1 class="h3 mb-5">New Email Alias</h1>
{% if user_custom_domains|length == 0 and not DISABLE_ALIAS_SUFFIX %}
<div class="row">
<div class="col p-1">
<div class="alert alert-primary" role="alert">
You might notice a random word after the dot(<em>.</em>) in the alias.
This part is to avoid a person taking all the "nice" aliases like <b>hello@{{ EMAIL_DOMAIN }}</b>,
<b>me@{{ EMAIL_DOMAIN }}</b>, etc. <br>
If you add your own domain, this restriction is removed and you can fully customize the alias. <br>
</div>
</div>
</div>
{% endif %}
<form method="post">
<div class="row mb-2">
<div class="col-sm-6 mb-1 p-1" style="min-width: 4em">
<input name="prefix" class="form-control"
type="text"
pattern="[0-9a-z-_]{1,}"
title="Only lowercase letter, number, dash (-), underscore (_) can be used in alias prefix."
placeholder="email alias, for example newsletter-123_xyz"
autofocus required>
<div class="small-text">
Only lowercase letter, number, dash (-), underscore (_) can be used.
</div>
</div>
<div class="col-sm-6 p-1">
<select class="form-control custom-select" name="suffix">
{% for suffix in suffixes %}
<option value="{{ suffix[1] }}">
{% if suffix[0] %}
{{ suffix[1] }} (your domain)
{% else %}
{{ suffix[1] }}
{% endif %}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-2">
<div class="col p-1">
<select class="form-control custom-select" name="mailbox">
{% for mailbox in mailboxes %}
<option value="{{ mailbox }}">
{{ mailbox }}
</option>
{% endfor %}
</select>
<div class="small-text">
The mailbox that owns this alias.
</div>
</div>
</div>
<div class="row mb-2">
<div class="col p-1">
<textarea name="note"
class="form-control"
rows="3"
placeholder="Note, can be anything to help you remember WHY you create this alias. This field is optional."></textarea>
</div>
</div>
<div class="row">
<div class="col p-1">
<button class="btn btn-primary mt-1">Create</button>
</div>
</div>
</form>
</div>
{% endblock %}

View File

@ -1,88 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "custom_domain" %}
{% block title %}
Custom Domains
{% endblock %}
{% block head %}
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="h3"> Custom Domains </h1>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">
This feature is only available in premium plan.
</div>
{% endif %}
<div class="alert alert-primary" role="alert">
If you own a domain, let's say <b>example.com</b>, you will be able to create aliases with this domain, for example
contact@example.com, help@example.com, etc with SimpleLogin. <br>
You could also enable <b>catch-all</b> feature that allows you to create aliases on-the-fly.
</div>
{% for custom_domain in custom_domains %}
<div class="card" style="max-width: 50rem">
<div class="card-body">
<h5 class="card-title">
<a href="{{ url_for('dashboard.domain_detail', custom_domain_id=custom_domain.id) }}">{{ custom_domain.domain }}</a>
{% if custom_domain.verified %}
<span class="cursor" data-toggle="tooltip" data-original-title="Domain Verified"></span>
{% else %}
<span class="cursor" data-toggle="tooltip" data-original-title="DNS Setup Needed">
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id) }}"
class="text-decoration-none">🚫
</a>
</span>
{% endif %}
</h5>
<h6 class="card-subtitle mb-2 text-muted">
Created {{ custom_domain.created_at | dt }} <br>
<span class="font-weight-bold">{{ custom_domain.nb_alias() }}</span> aliases.
</h6>
<a href="{{ url_for('dashboard.domain_detail', custom_domain_id=custom_domain.id) }}">Details ➡</a>
</div>
</div>
{% endfor %}
<hr>
<form method="post">
{{ new_custom_domain_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<label class="form-label">Domain</label>
<small>Please use full path domain, for ex <em>my-subdomain.my-domain.com</em></small>
{{ new_custom_domain_form.domain(class="form-control", placeholder="my-domain.com") }}
{{ render_field_errors(new_custom_domain_form.domain) }}
<button class="btn btn-lg btn-success mt-2">Create</button>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-custom-domain").on("click", function (e) {
notie.confirm({
text: "All aliases associated with this domain will be also deleted, " +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -1,31 +0,0 @@
{% extends 'default.html' %}
{% block title %}
Deleted Aliases
{% endblock %}
{% block head %}
{% endblock %}
{% block default_content %}
<div style="max-width: 60em; margin: auto">
<h1 class="h3 mb-5"> Deleted Aliases </h1>
{% if deleted_aliases|length == 0 %}
<div class="my-4 p-4 card">
You haven't deleted any alias.
</div>
{% endif %}
{% for deleted_alias in deleted_aliases %}
<div class="my-4 p-4 card border-light">
{{ deleted_alias.email }}
<div class="small-text">
Deleted {{ deleted_alias.created_at | dt }}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -1,114 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "directory" %}
{% block title %}
Directory
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="h3"> Directories </h1>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">
This feature is only available in premium plan.
</div>
{% endif %}
<div class="alert alert-primary" role="alert">
Directory allows you to create aliases <b>on the fly</b>. Simply use <br>
<div class="pl-3 py-2 bg-white">
<em>your_directory/<b>anything</b>@{{ EMAIL_DOMAIN }}</em> or <br>
<em>your_directory+<b>anything</b>@{{ EMAIL_DOMAIN }}</em> or <br>
<em>your_directory#<b>anything</b>@{{ EMAIL_DOMAIN }}</em> <br>
</div>
next time you need an email address. <br>
<em><b>anything</b></em> could really be anything, it's up to you to invent the most creative alias 😉. <br>
<em>your_directory</em> is the name of one of your directories. <br><br>
You can use the directory feature on the following domains:
{% for alias_domain in ALIAS_DOMAINS %}
<div class="font-weight-bold">{{ alias_domain }} </div>
{% endfor %}
<div class="h4 text-primary mt-3">
The alias will be created the first time it receives an email.
</div>
</div>
{% for dir in dirs %}
<div class="card" style="max-width: 50rem">
<div class="card-body">
<h5 class="card-title">
{{ dir.name }}
</h5>
<h6 class="card-subtitle mb-2 text-muted">
Created {{ dir.created_at | dt }} <br>
<span class="font-weight-bold">{{ dir.nb_alias() }}</span> aliases.
</h6>
</div>
<div class="card-footer p-0">
<div class="row">
<div class="col">
<form method="post">
<input type="hidden" name="form-name" value="delete">
<input type="hidden" class="dir-name" value="{{ dir.name }}">
<input type="hidden" name="dir-id" value="{{ dir.id }}">
<span class="card-link btn btn-link float-right text-danger delete-dir">
Delete
</span>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
{% if dirs|length > 0 %}
<hr>
{% endif %}
<form method="post" class="mt-6">
{{ new_dir_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<div class="font-weight-bold">Directory Name</div>
<div class="small-text">
Directory name must be at least 3 characters.
Only lowercase letter, number, dash (-), underscore (_) can be used.
</div>
{{ new_dir_form.name(class="form-control", placeholder="my-directory",
pattern="[0-9a-z-_]{3,}",
title="Only letter, number, dash (-), underscore (_) can be used. Directory name must be at least 3 characters.") }}
{{ render_field_errors(new_dir_form.name) }}
<button class="btn btn-lg btn-success mt-2">Create</button>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-dir").on("click", function (e) {
let directory = $(this).parent().find(".dir-name").val();
notie.confirm({
text: `All aliases associated with <b>${directory}</b> directory will be also deleted, ` +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -1,35 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "custom_domain" %}
{% block default_content %}
<div class="row">
<div class="col-lg-3 order-lg-1 mb-4">
<div class="list-group list-group-transparent mb-0">
<a href="{{ url_for('dashboard.domain_detail', custom_domain_id=custom_domain.id) }}"
class="list-group-item list-group-item-action {{ 'active' if domain_detail_page == 'info' }}">
<span class="icon mr-3"><i class="fe fe-flag"></i></span>Info
</a>
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id) }}"
class="list-group-item list-group-item-action {{ 'active' if domain_detail_page == 'dns' }}">
<span class="icon mr-3"><i class="fe fe-cloud"></i></span>DNS
</a>
</div>
</div>
<div class="col-lg-9">
<div class="card">
<div class="card-body">
<div class="text-wrap p-lg-6">
{% block domain_detail_content %}
{% endblock %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,198 +0,0 @@
{% extends 'dashboard/domain_detail/base.html' %}
{% set domain_detail_page = "dns" %}
{% block title %}
{{ custom_domain.domain }} DNS
{% endblock %}
{% block domain_detail_content %}
<div class="bg-white p-4" style="max-width: 60rem; margin: auto">
<h1 class="h3"> {{ custom_domain.domain }} </h1>
<div class="">Please follow the steps below to set up your domain.</div>
<div class="small-text mb-5">
DNS changes could take up to 24 hours to propagate. In practice, it's a lot faster though (~1
minute or in our experience).
</div>
<div id="mx-form">
<div class="font-weight-bold">1. MX record
{% if custom_domain.verified %}
<span class="cursor" data-toggle="tooltip" data-original-title="MX Record Verified"></span>
{% else %}
<span class="cursor" data-toggle="tooltip" data-original-title="MX Record Not Verified">🚫 </span>
{% endif %}
</div>
<div class="mb-2">Add the following MX DNS record to your domain. <br>
Please note that there's a point (<em>.</em>) at the end target addresses. <br>
Also some domain registrars (Namecheap, CloudFlare, etc) might use <em>@</em> for the root domain.
</div>
{% for priority, email_server in EMAIL_SERVERS_WITH_PRIORITY %}
<div class="mb-3 p-3" style="background-color: #eee">
Domain: <em>{{ custom_domain.domain }}</em> or <em>@</em> <br>
Priority: {{ priority }} <br>
Target: <em>{{ email_server }}</em> <br>
</div>
{% endfor %}
<form method="post" action="#mx-form">
<input type="hidden" name="form-name" value="check-mx">
{% if custom_domain.verified %}
<button type="submit" class="btn btn-outline-primary">
Re-verify
</button>
{% else %}
<button type="submit" class="btn btn-primary">
Verify
</button>
{% endif %}
</form>
{% if not mx_ok %}
<div class="text-danger mt-4">
Your DNS is not correctly set. The MX record we obtain is:
<div class="mb-3 p-3" style="background-color: #eee">
{% if not mx_errors %}
(Empty)
{% endif %}
{% for r in mx_errors %}
{{ r }} <br>
{% endfor %}
</div>
{% if custom_domain.verified %}
Please make sure to fix this ASAP - your aliases might not work properly.
{% endif %}
</div>
{% endif %}
</div>
<hr>
<div id="spf-form">
<div class="font-weight-bold">2. SPF (Optional)
{% if custom_domain.spf_verified %}
<span class="cursor" data-toggle="tooltip" data-original-title="SPF Verified"></span>
{% else %}
<span class="cursor" data-toggle="tooltip" data-original-title="SPF Not Verified">🚫 </span>
{% endif %}
</div>
<div>
SPF <a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework" target="_blank">(Wikipedia↗)</a> is an email
authentication method
designed to detect forging sender addresses during the delivery of the email. <br>
Setting up SPF is highly recommended to reduce the chance your emails ending up in the recipient's Spam folder.
</div>
<div class="mb-2">Add the following TXT DNS record to your domain</div>
<div class="mb-2 p-3" style="background-color: #eee">
Domain: <em>{{ custom_domain.domain }}</em> or <em>@</em> <br>
Value:
<em>
{{ spf_record }}
</em>
</div>
<form method="post" action="#spf-form">
<input type="hidden" name="form-name" value="check-spf">
{% if custom_domain.spf_verified %}
<button type="submit" class="btn btn-outline-primary">
Re-verify
</button>
{% else %}
<button type="submit" class="btn btn-primary">
Verify
</button>
{% endif %}
</form>
{% if not spf_ok %}
<div class="text-danger mt-4">
Your DNS is not correctly set. The TXT record we obtain is:
<div class="mb-3 p-3" style="background-color: #eee">
{% if not spf_errors %}
(Empty)
{% endif %}
{% for r in spf_errors %}
{{ r }} <br>
{% endfor %}
</div>
{% if custom_domain.spf_verified %}
Without SPF setup, emails you sent from your alias might end up in Spam/Junk folder.
{% endif %}
</div>
{% endif %}
</div>
<hr>
<div id="dkim-form">
<div class="font-weight-bold">3. DKIM (Optional)
{% if custom_domain.dkim_verified %}
<span class="cursor" data-toggle="tooltip" data-original-title="SPF Verified"></span>
{% else %}
<span class="cursor" data-toggle="tooltip" data-original-title="DKIM Not Verified">🚫 </span>
{% endif %}
</div>
<div>
DKIM <a href="https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail" target="_blank">(Wikipedia↗)</a> is an
email
authentication method
designed to avoid email spoofing. <br>
Setting up DKIM is highly recommended to reduce the chance your emails ending up in the recipient's Spam folder.
</div>
<div class="mb-2">Add the following TXT DNS record to your domain</div>
<div class="mb-2 p-3" style="background-color: #eee">
Domain: <em>dkim._domainkey.{{ custom_domain.domain }}</em> <br>
Value:
<em style="overflow-wrap: break-word">
{{ dkim_record }}
</em>
</div>
<form method="post" action="#dkim-form">
<input type="hidden" name="form-name" value="check-dkim">
{% if custom_domain.dkim_verified %}
<button type="submit" class="btn btn-outline-primary">
Re-verify
</button>
{% else %}
<button type="submit" class="btn btn-primary">
Verify
</button>
{% endif %}
</form>
{% if not dkim_ok %}
<div class="text-danger mt-4">
Your DNS is not correctly set.
{% if dkim_errors %}
The TXT record we obtain for
<em>dkim._domainkey.{{ custom_domain.domain }}</em> is:
<div class="mb-3 p-3" style="background-color: #eee">
{% for r in dkim_errors %}
{{ r }} <br>
{% endfor %}
</div>
{% endif %}
{% if custom_domain.dkim_verified %}
Without DKIM setup, emails you sent from your alias might end up in Spam/Junk folder.
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,86 +0,0 @@
{% extends 'dashboard/domain_detail/base.html' %}
{% set domain_detail_page = "info" %}
{% block title %}
{{ custom_domain.domain }} Info
{% endblock %}
{% block domain_detail_content %}
<h1 class="h3"> {{ custom_domain.domain }}
{% if custom_domain.verified %}
<span class="cursor" data-toggle="tooltip" data-original-title="DNS Setup OK"></span>
{% else %}
<span class="cursor" data-toggle="tooltip" data-original-title="DNS Setup Needed">
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id) }}"
class="text-decoration-none">🚫
</a>
</span>
{% endif %}
</h1>
<div class="small-text">Created {{ custom_domain.created_at | dt }}</div>
{{ nb_alias }} aliases
<hr>
<div>Catch All</div>
<div class="small-text">
This feature allows you to create aliases <b>on the fly</b>.
Simply use <em>anything@{{ custom_domain.domain }}</em>
next time you need an email address. <br>
The alias will be created the first time it receives an email.
</div>
<div>
<form method="post">
<input type="hidden" name="form-name" value="switch-catch-all">
<label class="custom-switch cursor mt-2 pl-0"
data-toggle="tooltip"
{% if custom_domain.catch_all %}
title="Disable catch-all"
{% else %}
title="Enable catch-all"
{% endif %}
>
<input type="checkbox" class="custom-switch-input"
{{ "checked" if custom_domain.catch_all else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</form>
</div>
<hr>
<h3 class="mb-0">Delete Domain</h3>
<div class="small-text mb-3">Please note that this operation is irreversible.
All aliases associated with this domain will be also deleted
</div>
<form method="post">
<input type="hidden" name="form-name" value="delete">
<span class="delete-custom-domain btn btn-outline-danger">Delete domain</span>
</form>
{% endblock %}
{% block script %}
<script>
$(".custom-switch-input").change(function (e) {
$(this).closest("form").submit();
});
$(".delete-custom-domain").on("click", function (e) {
notie.confirm({
text: "All aliases associated with <b>{{ custom_domain.domain }}</b> will be also deleted, " +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -1,383 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "dashboard" %}
{% block head %}
<style>
.alias-activity {
font-weight: 600;
font-size: 14px;
}
.btn-group-border-left {
border-left: 1px #fbfbfb4f solid;
}
</style>
{% endblock %}
{% block title %}
Alias
{% endblock %}
{% block default_content %}
<div class="page-header row" style="margin-top: 0rem">
<div class="col-lg-3 col-sm-12 p-0 mt-1">
<form method="get">
<input type="search" name="query" autofocus placeholder="Enter to search for alias" class="form-control shadow"
value="{{ query }}">
</form>
</div>
<div class="col-lg-5 offset-lg-4 pr-0 mt-1">
<div class="btn-group float-right" role="group">
<form method="post">
<input type="hidden" name="form-name" value="create-custom-email">
<button data-toggle="tooltip"
title="Create a custom alias"
class="btn btn-primary mr-2"><i class="fa fa-plus"></i> New Email Alias
</button>
</form>
<div class="btn-group" role="group">
<form method="post">
<input type="hidden" name="form-name" value="create-random-email">
<button data-toggle="tooltip"
title="Create a totally random alias"
class="btn btn-success"><i class="fa fa-random"></i> Random Alias
</button>
</form>
<button id="btnGroupDrop1" type="button" class="btn btn-success dropdown-toggle btn-group-border-left"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
</button>
<div class="dropdown-menu dropdown-menu-right border-left" aria-labelledby="btnGroupDrop1">
<div class="">
<form method="post">
<input type="hidden" name="form-name" value="create-random-email">
<input type="hidden" name="generator_scheme" value="{{ AliasGeneratorEnum.word.value }}">
<button class="dropdown-item">By Random Words</button>
</form>
</div>
<div class="">
<form method="post">
<input type="hidden" name="form-name" value="create-random-email">
<input type="hidden" name="generator_scheme" value="{{ AliasGeneratorEnum.uuid.value }}">
<button class="dropdown-item">By UUID</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
{% for alias_info in aliases %}
{% set gen_email = alias_info.gen_email %}
<div class="col-12 col-lg-6">
<div class="card p-4 shadow-sm {% if alias_info.highlight %} highlight-row {% endif %} ">
<div class="row">
<div class="col-8">
<span class="clipboard cursor mb-0"
{% if loop.index ==1 %}
data-intro="This is an <em>alias</em>. <br><br>
<b>All</b> emails sent to an alias will be <em>forwarded</em> to your inbox. <br><br>
Alias is a great way to hide your personal email address so feel free to
use it whenever possible, for example when signing up for a newsletter or creating a new account on a suspicious website 😎"
data-step="2"
{% endif %}
{% if gen_email.enabled %}
data-toggle="tooltip"
title="Copy to clipboard"
data-clipboard-text="{{ gen_email.email }}"
{% endif %}
>
<span class="font-weight-bold">{{ gen_email.email }}</span>
{% if gen_email.enabled %}
<span class="btn btn-sm btn-success copy-btn">
Copy
</span>
{% endif %}
</span>
</div>
<div class="col text-right">
<form method="post">
<input type="hidden" name="form-name" value="switch-email-forwarding">
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
<label class="custom-switch cursor"
data-toggle="tooltip"
{% if gen_email.enabled %}
title="Block Alias"
{% else %}
title="Unblock Alias"
{% endif %}
{% if loop.index ==1 %}
data-intro="By turning off an alias, emails sent to this alias will <em>not</em>
be forwarded to your inbox. <br><br>
This should be used with care as others might
not be able to reach you after ...
"
data-step="3"
{% endif %}
style="padding-left: 0px"
>
<input type="hidden" name="alias" class="alias" value="{{ gen_email.email }}">
<input type="checkbox" class="custom-switch-input"
{{ "checked" if gen_email.enabled else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</form>
</div>
</div>
<hr class="my-2">
<p class="small-text">
Created {{ gen_email.created_at | dt }}
{% if alias_info.highlight %}
- <span class="font-weight-bold text-success small-text">New</span>
{% endif %}
</p>
<div class="" style="font-size: 12px">
<span class="alias-activity">{{ alias_info.nb_forward }}</span> forwards,
<span class="alias-activity">{{ alias_info.nb_blocked }}</span> blocks,
<span class="alias-activity">{{ alias_info.nb_reply }}</span> replies
<a href="{{ url_for('dashboard.alias_log', alias_id=gen_email.id) }}"
class="btn btn-sm btn-link">
See All Activity &nbsp;
</a>
</div>
{% if mailboxes|length > 1 %}
<form method="post">
<div class="small-text mt-2">Current mailbox</div>
<div class="d-flex">
<div class="flex-grow-1 mr-2">
<select class="form-control form-control-sm custom-select" name="mailbox">
{% for mailbox in mailboxes %}
<option value="{{ mailbox }}" {% if mailbox == alias_info.mailbox.email %} selected {% endif %}>
{{ mailbox }}
</option>
{% endfor %}
</select>
</div>
<div class="">
<input type="hidden" name="form-name" value="set-mailbox">
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
<button class="btn btn-sm btn-outline-info w-100">
Update
</button>
</div>
</div>
</form>
{% elif alias_info.mailbox != None and alias_info.mailbox.email != current_user.email %}
<div class="small-text">
Owned by <b>{{ alias_info.mailbox.email }}</b> mailbox
</div>
{% endif %}
<form method="post">
<div class="d-flex mt-2">
<div class="flex-grow-1 mr-2">
<textarea
name="note"
class="form-control"
rows="2"
placeholder="Alias Note.">{{ gen_email.note or "" }}</textarea>
</div>
<div class="">
<input type="hidden" name="form-name" value="set-note">
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
<button class="btn btn-sm btn-outline-success w-100">
Save
</button>
</div>
</div>
</form>
<div class="row mt-3">
<div class="col">
{% if gen_email.enabled %}
<a href="{{ url_for('dashboard.alias_contact_manager', alias_id=gen_email.id) }}"
{% if alias_info.show_intro_test_send_email %}
data-intro="Not only alias can receive emails, it can <em>send</em> emails too! <br><br>
You can add a new <em>contact</em> to for your alias here. <br><br>
To send an email to your contact, SimpleLogin will create a <em>special</em> email address. <br><br>
Sending an email to this email address will <em>forward</em> the email to your contact"
data-step="4"
{% endif %}
class="btn btn-sm btn-outline-primary"
data-toggle="tooltip"
title="Not only an alias can receive emails, it can send emails too"
>
Send Email&nbsp; &nbsp;<i class="fe fe-send"></i>
</a>
{% endif %}
</div>
<div class="col">
<form method="post">
<input type="hidden" name="form-name" value="delete-email">
<input type="hidden" name="gen-email-id" value="{{ gen_email.id }}">
<input type="hidden" name="alias" class="alias" value="{{ gen_email.email }}">
<span class="delete-email btn btn-link btn-sm float-right text-danger">
Delete&nbsp; &nbsp;<i class="dropdown-icon fe fe-trash-2 text-danger"></i>
</span>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% if client_users %}
<div class="page-header row">
<h3 class="page-title col"
data-intro="Here you can find the list of website/app on which
you have used the <em>Connect with SimpleLogin</em> button <br><br>
You also see what information that SimpleLogin has communicated to these website/app when you sign in."
data-step="5"
>
Apps
</h3>
</div>
<div class="row row-cards row-deck mt-4">
<div class="col-12">
<div class="card">
<div class="table-responsive">
<table class="table table-hover table-outline table-vcenter text-nowrap card-table">
<thead>
<tr>
<th>
App
</th>
<th>
Info
<i class="fe fe-help-circle" data-toggle="tooltip"
title="Info sent to this app/website"></i>
</th>
<th class="text-center">
First used
<i class="fe fe-help-circle" data-toggle="tooltip"
title="The first time you have used the SimpleLogin on this app/website"></i>
</th>
<!--<th class="text-center">Last used</th>-->
</tr>
</thead>
<tbody>
{% for client_user in client_users %}
<tr>
<td>
{{ client_user.client.name }}
</td>
<td>
{% for scope, val in client_user.get_user_info().items() %}
<div>
{% if scope == "email" %}
Email: <a href="mailto:{{ val }}">{{ val }}</a>
{% elif scope == "name" %}
Name: {{ val }}
{% endif %}
</div>
{% endfor %}
</td>
<td class="text-center">
{{ client_user.created_at | dt }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block script %}
<script>
var clipboard = new ClipboardJS('.clipboard');
var introShown = store.get("introShown");
if ("yes" !== introShown) {
// only show intro when screen is big enough to show "developer" tab
if (window.innerWidth >= 1024) {
introJs().start();
store.set("introShown", "yes")
}
}
$(".delete-email").on("click", function (e) {
let alias = $(this).parent().find(".alias").val();
notie.confirm({
text: `Once <b>${alias}</b> is deleted, people/apps ` +
"who used to contact you via this alias cannot reach you any more," +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
$(".trigger-email").on("click", function (e) {
notie.confirm({
text: "SimpleLogin server will send an email to this alias " +
"and it will arrive to your inbox, please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
$(".custom-switch-input").change(function (e) {
var message = "";
let alias = $(this).parent().find(".alias").val();
if (e.target.checked) {
message = `After this, you will start receiving email sent to <b>${alias}</b>, please confirm.`;
} else {
message = `After this, you will stop receiving email sent to <b>${alias}</b>, please confirm.`;
}
notie.confirm({
text: message,
cancelCallback: () => {
// reset to the original value
var oldValue = !$(this).prop("checked");
$(this).prop("checked", oldValue);
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
})
</script>
{% endblock %}

View File

@ -1,29 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "dashboard" %}
{% block title %}
Lifetime Licence
{% endblock %}
{% block default_content %}
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
<h1 class="h2">Lifetime Licence</h1>
<div class="mb-4">
If you have a lifetime licence, please paste it here. <br>
For information, we offer free premium account for education (student, professor or technical staff working at
an educational institute). <br>
Drop us an email at <a href="mailto:hi@simplelogin.io">hi@simplelogin.io</a> with your student ID or certificate to get the lifetime licence.
</div>
<form method="post">
{{ coupon_form.csrf_token }}
{{ coupon_form.code(class="form-control", placeholder="Licence Code") }}
{{ render_field_errors(coupon_form.code) }}
<button class="btn btn-success mt-2">Apply</button>
</form>
</div>
{% endblock %}

View File

@ -1,138 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "mailbox" %}
{% block title %}
Mailboxes
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="h3"> Mailboxes </h1>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">
This feature is only available in premium plan.
</div>
{% endif %}
<div class="alert alert-primary" role="alert">
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose the
mailbox that <em>owns</em> this alias, i.e: <br>
- all emails sent to this alias will be forwarded to this mailbox <br>
- from this mailbox, you can reply/send emails from the alias. <br><br>
When you signed up, a mailbox is automatically created with your email <b>{{ current_user.email }}</b>
<br><br>
The mailbox doesn't have to be your email: it can be your friend's email
if you want to create aliases for your buddy.
</div>
{% for mailbox in mailboxes %}
<div class="card" style="max-width: 50rem">
<div class="card-body">
<h5 class="card-title">
{{ mailbox.email }}
{% if mailbox.verified %}
<span class="cursor" data-toggle="tooltip" data-original-title="Mailbox Verified"></span>
{% else %}
<span class="cursor" data-toggle="tooltip" data-original-title="Mailbox Not Verified">
🚫
</span>
{% endif %}
{% if mailbox.pgp_finger_print %}
<span class="cursor" data-toggle="tooltip" data-original-title="PGP Enabled">🗝</span>
{% endif %}
{% if mailbox.id == current_user.default_mailbox_id %}
<div class="badge badge-primary float-right" data-toggle="tooltip"
title="When a new random alias is created, it belongs to the default mailbox">Default Mailbox
</div>
{% endif %}
</h5>
<h6 class="card-subtitle mb-2 text-muted">
Created {{ mailbox.created_at | dt }} <br>
<span class="font-weight-bold">{{ mailbox.nb_alias() }}</span> aliases. <br>
</h6>
<a href="{{ url_for('dashboard.mailbox_detail_route', mailbox_id=mailbox.id) }}">Edit ➡</a>
</div>
<div class="card-footer p-0">
<div class="row">
{% if mailbox.verified %}
<div class="col">
<form method="post">
<input type="hidden" name="form-name" value="set-default">
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
<button class="card-link btn btn-link
{% if mailbox.id == current_user.default_mailbox_id %} disabled {% endif %}"
>
Set As Default Mailbox
</button>
</form>
</div>
{% endif %}
<div class="col">
<form method="post">
<input type="hidden" name="form-name" value="delete">
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
<span class="card-link btn btn-link text-danger float-right delete-mailbox
{% if mailbox.id == current_user.default_mailbox_id %} disabled {% endif %}">
Delete
</span>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
{% if mailboxs|length > 0 %}
<hr>
{% endif %}
<form method="post" class="mt-6">
{{ new_mailbox_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<div class="font-weight-bold">Email</div>
<div class="small-text">
A verification email will be sent to this email to make sure you have access to this email.
</div>
{{ new_mailbox_form.email(class="form-control", placeholder="email@example.com") }}
{{ render_field_errors(new_mailbox_form.email) }}
<button class="btn btn-lg btn-success mt-2">Create</button>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-mailbox").on("click", function (e) {
let mailbox = $(this).parent().find(".mailbox").val();
notie.confirm({
text: `All aliases owned by this mailbox <b>${mailbox}</b> will be also deleted, ` +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -1,100 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "mailbox" %}
{% block title %}
Mailbox {{ mailbox.email }}
{% endblock %}
{% block default_content %}
<div class="col-md-8 offset-md-2 pb-3">
<h1 class="h3">{{ mailbox.email }}
{% if mailbox.verified %}
<span class="cursor" data-toggle="tooltip" data-original-title="Mailbox Verified"></span>
{% else %}
<span class="cursor" data-toggle="tooltip" data-original-title="Mailbox Not Verified">
🚫
</span>
{% endif %}
{% if mailbox.pgp_finger_print %}
<span class="cursor" data-toggle="tooltip" data-original-title="PGP Enabled">🗝</span>
{% endif %}
</h1>
{% if not mailbox.verified %}
<div class="alert alert-info">
Mailbox not verified, please check your inbox/spam folder for the verification email.
<br>
To receive the verification email again, you can delete and re-add the mailbox.
</div>
{% endif %}
<!-- Change email -->
<div class="card">
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="form-name" value="update-email">
{{ change_email_form.csrf_token }}
<div class="card-body">
<div class="card-title">
Change Mailbox Address
</div>
<div class="form-group">
<label class="form-label">Address</label>
<!-- Not allow user to change mailbox if there's a pending change -->
{{ change_email_form.email(class="form-control", value=mailbox.email, readonly=pending_email != None) }}
{{ render_field_errors(change_email_form.email) }}
{% if pending_email %}
<div class="mt-2">
<span class="text-danger">Pending change: {{ pending_email }}</span>
<a href="{{ url_for('dashboard.cancel_mailbox_change_route', mailbox_id=mailbox.id) }}"
class="btn btn-secondary btn-sm">
Cancel mailbox change
</a>
</div>
{% endif %}
</div>
<button class="btn btn-primary">Change</button>
</div>
</form>
</div>
<!-- END Change email -->
<!-- Change PGP Public key -->
{% if current_user.can_use_pgp %}
<div class="card">
<form method="post">
<input type="hidden" name="form-name" value="pgp">
<div class="card-body">
<div class="card-title">
Pretty Good Privacy (PGP)
<div class="small-text">
By importing your PGP Public Key into SimpleLogin, all emails sent to {{mailbox.email}} are <b>encrypted</b> with your key.
</div>
</div>
<div class="form-group">
<label class="form-label">PGP Public Key</label>
<textarea name="pgp" class="form-control" rows=10 placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----">{{mailbox.pgp_public_key or ""}}</textarea>
</div>
<button class="btn btn-primary" name="action" value="save">Save</button>
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
</div>
</form>
</div>
{% endif %}
<!-- END PGP Public key -->
</div>
{% endblock %}

View File

@ -1,28 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "setting" %}
{% block title %}
Cancel MFA
{% endblock %}
{% block default_content %}
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
<h1 class="h2">Multi Factor Authentication</h1>
<p>
To cancel MFA, please enter the 6-digit number in your TOTP application (Google Authenticator, Authy, etc) here.
</p>
<form method="post">
{{ otp_token_form.csrf_token }}
<div class="font-weight-bold mt-5">Token</div>
<div class="small-text">The 6-digit number displayed on your phone.</div>
{{ otp_token_form.token(class="form-control", autofocus="true") }}
{{ render_field_errors(otp_token_form.token) }}
<button class="btn btn-lg btn-danger mt-2">Cancel MFA</button>
</form>
</div>
{% endblock %}

View File

@ -1,51 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "setting" %}
{% block title %}
MFA Setup
{% endblock %}
{% block head %}
<script src="{{ url_for('static', filename='node_modules/qrious/dist/qrious.min.js') }}"></script>
{% endblock %}
{% block default_content %}
<div class="bg-white p-6" style="max-width: 60em; margin: auto">
<h1 class="h2">Multi Factor Authentication</h1>
<p>Please open a TOTP application (Google Authenticator, Authy, etc)
on your smartphone and scan the following QR Code:
</p>
<canvas id="qr"></canvas>
<script>
(function () {
var qr = new QRious({
element: document.getElementById('qr'),
value: '{{otp_uri}}'
});
})();
</script>
<div class="mt-3 mb-2">
Or you can use the manual entry with the following key:
</div>
<div class="mb-3 p-3" style="background-color: #eee">
{{ current_user.otp_secret }}
</div>
<form method="post">
{{ otp_token_form.csrf_token }}
<div class="font-weight-bold mt-5">Token</div>
<div class="small-text">Please enter the 6-digit number displayed on your phone.</div>
{{ otp_token_form.token(class="form-control", placeholder="") }}
{{ render_field_errors(otp_token_form.token) }}
<button class="btn btn-lg btn-success mt-2">Validate</button>
</form>
</div>
{% endblock %}

View File

@ -1,101 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "dashboard" %}
{% block title %}
Pricing
{% endblock %}
{% block head %}
<script src="https://cdn.paddle.com/paddle/paddle.js"></script>
<script>
if (window.Paddle === undefined) {
console.log("cannot load Paddle from CDN");
document.write('<script src="/static/vendor/paddle.js"><\/script>')
}
</script>
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col-sm-6 col-lg-6">
<div class="card">
<div class="card-body text-center">
<div class="h3">Premium</div>
<ul class="list-unstyled leading-loose mb-3">
<li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i> Unlimited Alias</li>
<li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Custom Domain
</li>
<li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Catch-all (or wildcard) alias
</li>
<li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Directory (or Username)
</li>
<li><i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Multiple Mailboxes
</li>
</ul>
<div class="small-text">More info on our <a href="https://simplelogin.io/pricing" target="_blank">Pricing
Page <i class="fe fe-external-link"></i>
</a></div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-6">
<div class="display-6 my-3">
🔐 Secure payments by
<a href="https://paddle.com" target="_blank">Paddle<i class="fe fe-external-link"></i></a></li>
</a>
</div>
{% if current_user.is_cancel() %}
<div class="alert alert-primary" role="alert">
You have an active subscription until {{current_user.next_bill_date()}}. <br>
Please note that if you re-subscribe now, this will be a completely
new subscription and
your payment method will be charged <b>immediately</b>.
</div>
{% endif %}
<div class="mb-3">
Paddle supported payment methods include bank cards (Mastercard, Visa, American Express, etc) or PayPal. <br>
Send us an email at <a href="mailto:hi@simplelogin.io">hi@simplelogin.io</a> if you need other payment options
(e.g. IBAN transfer).
</div>
<button class="btn btn-success" onclick="upgrade({{ PADDLE_MONTHLY_PRODUCT_ID }})">
Monthly <br>
$2.99/month
</button>
<button class="btn btn-primary" onclick="upgrade({{ PADDLE_YEARLY_PRODUCT_ID }})">
Yearly <br>
$29.99/year
</button>
<hr class="my-6">
If you have a lifetime licence, please go to this page to apply your licence code.
<a href="{{ url_for('dashboard.lifetime_licence') }}">Lifetime Licence</a>
</div>
</div>
<script type="text/javascript">
Paddle.Setup({vendor: {{ PADDLE_VENDOR_ID }}});
function upgrade(productId) {
Paddle.Checkout.open({
product: productId,
email: "{{ current_user.email }}",
success: "{{ success_url }}"
});
}
</script>
{% endblock %}

View File

@ -1,236 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "setting" %}
{% block title %}
Settings
{% endblock %}
{% block default_content %}
<div class="col-md-8 offset-md-2 pb-3">
<!-- Change email -->
<div class="card">
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="form-name" value="update-email">
{{ change_email_form.csrf_token }}
<div class="card-body">
<div class="card-title">
Change Email Address
</div>
<div class="form-group">
<label class="form-label">Email</label>
<!-- Not allow user to change email if there's a pending change -->
{{ change_email_form.email(class="form-control", value=current_user.email, readonly=pending_email != None) }}
{{ render_field_errors(change_email_form.email) }}
{% if pending_email %}
<div class="mt-2">
<span class="text-danger">Pending email change: {{ pending_email }}</span>
<a href="{{ url_for('dashboard.resend_email_change') }}" class="btn btn-secondary btn-sm">Resend
confirmation email</a>
<a href="{{ url_for('dashboard.cancel_email_change') }}" class="btn btn-secondary btn-sm">Cancel email
change</a>
</div>
{% endif %}
</div>
<button class="btn btn-primary">Change Email</button>
</div>
</form>
</div>
<!-- END Change email -->
<!-- Change name & profile picture -->
<div class="card">
<form method="post" enctype="multipart/form-data">
{{ form.csrf_token }}
<input type="hidden" name="form-name" value="update-profile">
<div class="card-body">
<div class="card-title">
Change Profile
</div>
<div class="form-group">
<label class="form-label">Name</label>
{{ form.name(class="form-control", value=current_user.name) }}
{{ render_field_errors(form.name) }}
</div>
<div class="form-group">
<div class="form-label">Profile picture</div>
{{ form.profile_picture(class="form-control-file") }}
{{ render_field_errors(form.profile_picture) }}
{% if current_user.profile_picture_id %}
<img src="{{ current_user.profile_picture_url() }}" class="profile-picture">
{% endif %}
</div>
<button class="btn btn-primary">Update</button>
</div>
</form>
</div>
<!-- END change name & profile picture -->
<div class="card">
<div class="card-body">
<div class="card-title">Multi-Factor Authentication (MFA)
<div class="small-text mt-1 mb-3">
Secure your account with Multi-Factor Authentication. <br>
This requires having applications like Google Authenticator, Authy, FreeOTP, etc.
</div>
</div>
{% if not current_user.enable_otp %}
<a href="{{ url_for('dashboard.mfa_setup') }}" class="btn btn-outline-primary">Enable</a>
{% else %}
<a href="{{ url_for('dashboard.mfa_cancel') }}" class="btn btn-outline-danger">Cancel MFA</a>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title">
Change password
<div class="small-text mt-1 mb-3">
You will receive an email containing instructions on how to change password.
</div>
</div>
<form method="post">
<input type="hidden" name="form-name" value="change-password">
<button class="btn btn-outline-primary">Change password</button>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title">Random Alias
<div class="small-text mt-1 mb-3">Choose how to create your email alias by default</div>
</div>
<form method="post" class="form-inline">
<input type="hidden" name="form-name" value="change-alias-generator">
<select class="custom-select mr-sm-2" name="alias-generator-scheme">
<option value="{{ AliasGeneratorEnum.word.value }}"
{% if current_user.alias_generator == AliasGeneratorEnum.word.value %} selected {% endif %} >Based on
Random {{ AliasGeneratorEnum.word.name.capitalize() }}</option>
<option value="{{ AliasGeneratorEnum.uuid.value }}"
{% if current_user.alias_generator == AliasGeneratorEnum.uuid.value %} selected {% endif %} >Based
on {{ AliasGeneratorEnum.uuid.name.upper() }}</option>
</select>
<button class="btn btn-outline-primary">Update Preference</button>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title" id="notification">Newsletters
<div class="small-text mt-1 mb-3">
Every now and then we can send you an email
to let you know about a new feature that might be useful to you.
</div>
</div>
<form method="post">
<input type="hidden" name="form-name" value="notification-preference">
<div class="form-check">
<input type="checkbox" id="notification" name="notification" {% if current_user.notification %}
checked {% endif %} class="form-check-input">
<label for="notification">I want to receive your newsletter</label>
</div>
<button type="submit" class="btn btn-outline-primary">Submit</button>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title mb-3">Current Plan</div>
{% if current_user.get_subscription() %}
You are on the {{ current_user.get_subscription().plan_name() }} plan. <br>
<a href="{{ url_for('dashboard.billing') }}" class="btn btn-outline-primary">
Manage Subscription
</a>
{% elif manual_sub %}
You are on the Premium plan. The plan ends {{ manual_sub.end_at | dt }}.
{% elif current_user.lifetime %}
You have the lifetime licence.
{% elif current_user.in_trial() %}
You are in the trial period. The trial ends {{ current_user.trial_end | dt }}.
{% else %}
You are on the Free plan.
{% endif %}
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title">Deleted Aliases
<div class="small-text mt-1 mb-3" style="max-width: 40rem">
When an alias is deleted, all its activities are deleted and no emails can be sent to it. <br>
It is moved to another location and only used to check when new alias is created. <br>
This check is necessary to avoid someone else accidentally taking this alias. <br>
Because in this case, the other person might receive inadvertently information that belong to you. <br>
</div>
</div>
<a href="{{ url_for('dashboard.deleted_alias_route') }}" class="btn btn-outline-primary">
See deleted aliases
</a>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title">Export Data
<div class="small-text mt-1 mb-3">
You can download all aliases you have created on SimpleLogin along with other data.
</div>
</div>
<form method="post">
<input type="hidden" name="form-name" value="export-data">
<button class="btn btn-outline-info">Export Data</button>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title">Delete Account
<div class="small-text mt-1 mb-3">Please note that this operation is irreversible.
</div>
</div>
<form method="post">
<input type="hidden" name="form-name" value="delete-account">
<span class="delete-account btn btn-outline-danger">Delete account</span>
</form>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-account").on("click", function (e) {
notie.confirm({
text: "All your data including your aliases will be deleted, " +
"other people might not be able to reach you after, " +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -1,28 +0,0 @@
{% extends 'default.html' %}
{% set active_page = "dashboard" %}
{% block title %}
Block an alias
{% endblock %}
{% block default_content %}
<div class="col-md-6 offset-md-3 text-center bg-white p-3 mt-5">
<h1 class="h3">
Block alias
</h1>
<p>
You are about to block the alias <a href="mailto:{{alias}}">{{alias}}</a>
</p>
<p>
After this, you will stop receiving all emails sent to this alias, please confirm.
</p>
<form method="post">
<button class="btn btn-warning">Confirm</button>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,242 @@
import arrow
from flask import (
render_template,
request,
redirect,
url_for,
flash,
)
from flask_login import login_required, current_user
from app import email_utils
from app.config import (
URL,
FIRST_ALIAS_DOMAIN,
ALIAS_RANDOM_SUFFIX_LENGTH,
CONNECT_WITH_PROTON,
)
from app.dashboard.base import dashboard_bp
from app.dashboard.views.enter_sudo import sudo_required
from app.dashboard.views.mailbox_detail import ChangeEmailForm
from app.db import Session
from app.email_utils import (
email_can_be_used_as_mailbox,
personal_email_already_used,
)
from app.extensions import limiter
from app.jobs.export_user_data_job import ExportUserDataJob
from app.log import LOG
from app.models import (
BlockBehaviourEnum,
PlanEnum,
ResetPasswordCode,
EmailChange,
User,
Alias,
AliasGeneratorEnum,
SenderFormatEnum,
UnsubscribeBehaviourEnum,
)
from app.proton.utils import perform_proton_account_unlink
from app.utils import (
random_string,
CSRFValidationForm,
canonicalize_email,
)
@dashboard_bp.route("/account_setting", methods=["GET", "POST"])
@login_required
@sudo_required
@limiter.limit("5/minute", methods=["POST"])
def account_setting():
change_email_form = ChangeEmailForm()
csrf_form = CSRFValidationForm()
email_change = EmailChange.get_by(user_id=current_user.id)
if email_change:
pending_email = email_change.new_email
else:
pending_email = None
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(url_for("dashboard.setting"))
if request.form.get("form-name") == "update-email":
if change_email_form.validate():
# whether user can proceed with the email update
new_email_valid = True
new_email = canonicalize_email(change_email_form.email.data)
if new_email != current_user.email and not pending_email:
# check if this email is not already used
if personal_email_already_used(new_email) or Alias.get_by(
email=new_email
):
flash(f"Email {new_email} already used", "error")
new_email_valid = False
elif not email_can_be_used_as_mailbox(new_email):
flash(
"You cannot use this email address as your personal inbox.",
"error",
)
new_email_valid = False
# a pending email change with the same email exists from another user
elif EmailChange.get_by(new_email=new_email):
other_email_change: EmailChange = EmailChange.get_by(
new_email=new_email
)
LOG.w(
"Another user has a pending %s with the same email address. Current user:%s",
other_email_change,
current_user,
)
if other_email_change.is_expired():
LOG.d(
"delete the expired email change %s", other_email_change
)
EmailChange.delete(other_email_change.id)
Session.commit()
else:
flash(
"You cannot use this email address as your personal inbox.",
"error",
)
new_email_valid = False
if new_email_valid:
email_change = EmailChange.create(
user_id=current_user.id,
code=random_string(
60
), # todo: make sure the code is unique
new_email=new_email,
)
Session.commit()
send_change_email_confirmation(current_user, email_change)
flash(
"A confirmation email is on the way, please check your inbox",
"success",
)
return redirect(url_for("dashboard.account_setting"))
elif request.form.get("form-name") == "change-password":
flash(
"You are going to receive an email containing instructions to change your password",
"success",
)
send_reset_password_email(current_user)
return redirect(url_for("dashboard.account_setting"))
elif request.form.get("form-name") == "send-full-user-report":
if ExportUserDataJob(current_user).store_job_in_db():
flash(
"You will receive your SimpleLogin data via email shortly",
"success",
)
else:
flash("An export of your data is currently in progress", "error")
partner_sub = None
partner_name = None
return render_template(
"dashboard/account_setting.html",
csrf_form=csrf_form,
PlanEnum=PlanEnum,
SenderFormatEnum=SenderFormatEnum,
BlockBehaviourEnum=BlockBehaviourEnum,
change_email_form=change_email_form,
pending_email=pending_email,
AliasGeneratorEnum=AliasGeneratorEnum,
UnsubscribeBehaviourEnum=UnsubscribeBehaviourEnum,
partner_sub=partner_sub,
partner_name=partner_name,
FIRST_ALIAS_DOMAIN=FIRST_ALIAS_DOMAIN,
ALIAS_RAND_SUFFIX_LENGTH=ALIAS_RANDOM_SUFFIX_LENGTH,
connect_with_proton=CONNECT_WITH_PROTON,
)
def send_reset_password_email(user):
"""
generate a new ResetPasswordCode and send it over email to user
"""
# the activation code is valid for 1h
reset_password_code = ResetPasswordCode.create(
user_id=user.id, code=random_string(60)
)
Session.commit()
reset_password_link = f"{URL}/auth/reset_password?code={reset_password_code.code}"
email_utils.send_reset_password_email(user, reset_password_link)
def send_change_email_confirmation(user: User, email_change: EmailChange):
"""
send confirmation email to the new email address
"""
link = f"{URL}/auth/change_email?code={email_change.code}"
email_utils.send_change_email(user, email_change.new_email, link)
@dashboard_bp.route("/resend_email_change", methods=["GET", "POST"])
@limiter.limit("5/hour")
@login_required
@sudo_required
def resend_email_change():
form = CSRFValidationForm()
if not form.validate():
flash("Invalid request. Please try again", "warning")
return redirect(url_for("dashboard.setting"))
email_change = EmailChange.get_by(user_id=current_user.id)
if email_change:
# extend email change expiration
email_change.expired = arrow.now().shift(hours=12)
Session.commit()
send_change_email_confirmation(current_user, email_change)
flash("A confirmation email is on the way, please check your inbox", "success")
return redirect(url_for("dashboard.setting"))
else:
flash(
"You have no pending email change. Redirect back to Setting page", "warning"
)
return redirect(url_for("dashboard.setting"))
@dashboard_bp.route("/cancel_email_change", methods=["GET", "POST"])
@login_required
@sudo_required
def cancel_email_change():
form = CSRFValidationForm()
if not form.validate():
flash("Invalid request. Please try again", "warning")
return redirect(url_for("dashboard.setting"))
email_change = EmailChange.get_by(user_id=current_user.id)
if email_change:
EmailChange.delete(email_change.id)
Session.commit()
flash("Your email change is cancelled", "success")
return redirect(url_for("dashboard.setting"))
else:
flash(
"You have no pending email change. Redirect back to Setting page", "warning"
)
return redirect(url_for("dashboard.setting"))
@dashboard_bp.route("/unlink_proton_account", methods=["POST"])
@login_required
@sudo_required
def unlink_proton_account():
csrf_form = CSRFValidationForm()
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(url_for("dashboard.setting"))
perform_proton_account_unlink(current_user)
flash("Your Proton account has been unlinked", "success")
return redirect(url_for("dashboard.setting"))

View File

@ -1,17 +1,31 @@
import re from dataclasses import dataclass
from operator import or_
from flask import render_template, request, redirect, url_for, flash from flask import render_template, request, redirect, flash
from flask import url_for
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from sqlalchemy import and_, func, case
from wtforms import StringField, validators, ValidationError from wtforms import StringField, validators, ValidationError
from app.config import EMAIL_DOMAIN # Need to import directly from config to allow modification from the tests
from app import config, parallel_limiter
from app.dashboard.base import dashboard_bp from app.dashboard.base import dashboard_bp
from app.email_utils import get_email_part from app.db import Session
from app.extensions import db from app.email_utils import (
generate_reply_email,
parse_full_address,
)
from app.email_validation import is_valid_email
from app.errors import (
CannotCreateContactForReverseAlias,
ErrContactErrorUpgradeNeeded,
ErrAddressInvalid,
ErrContactAlreadyExists,
)
from app.log import LOG from app.log import LOG
from app.models import GenEmail, ForwardEmail from app.models import Alias, Contact, EmailLog, User
from app.utils import random_string from app.utils import sanitize_email, CSRFValidationForm
def email_validator(): def email_validator():
@ -31,118 +45,280 @@ def email_validator():
if email.find("<") + 1 < email.find(">"): if email.find("<") + 1 < email.find(">"):
email_part = email[email.find("<") + 1 : email.find(">")].strip() email_part = email[email.find("<") + 1 : email.find(">")].strip()
if re.match(r"^[A-Za-z0-9\.\+_-]+@[A-Za-z0-9\._-]+\.[a-zA-Z]*$", email_part): if not is_valid_email(email_part):
return raise ValidationError(message)
raise ValidationError(message)
return _check return _check
def create_contact(user: User, alias: Alias, contact_address: str) -> Contact:
"""
Create a contact for a user. Can be restricted for new free users by enabling DISABLE_CREATE_CONTACTS_FOR_FREE_USERS.
Can throw exceptions:
- ErrAddressInvalid
- ErrContactAlreadyExists
- ErrContactUpgradeNeeded - If DISABLE_CREATE_CONTACTS_FOR_FREE_USERS this exception will be raised for new free users
"""
if not contact_address:
raise ErrAddressInvalid("Empty address")
try:
contact_name, contact_email = parse_full_address(contact_address)
except ValueError:
raise ErrAddressInvalid(contact_address)
contact_email = sanitize_email(contact_email)
if not is_valid_email(contact_email):
raise ErrAddressInvalid(contact_email)
contact = Contact.get_by(alias_id=alias.id, website_email=contact_email)
if contact:
raise ErrContactAlreadyExists(contact)
if not user.can_create_contacts():
raise ErrContactErrorUpgradeNeeded()
contact = Contact.create(
user_id=alias.user_id,
alias_id=alias.id,
website_email=contact_email,
name=contact_name,
reply_email=generate_reply_email(contact_email, alias),
)
LOG.d(
"create reverse-alias for %s %s, reverse alias:%s",
contact_address,
alias,
contact.reply_email,
)
Session.commit()
return contact
class NewContactForm(FlaskForm): class NewContactForm(FlaskForm):
email = StringField( email = StringField(
"Email", validators=[validators.DataRequired(), email_validator()] "Email", validators=[validators.DataRequired(), email_validator()]
) )
@dashboard_bp.route("/alias_contact_manager/<alias_id>/", methods=["GET", "POST"]) @dataclass
@dashboard_bp.route( class ContactInfo(object):
"/alias_contact_manager/<alias_id>/<int:forward_email_id>", methods=["GET", "POST"] contact: Contact
)
nb_forward: int
nb_reply: int
latest_email_log: EmailLog
def get_contact_infos(
alias: Alias, page=0, contact_id=None, query: str = ""
) -> [ContactInfo]:
"""if contact_id is set, only return the contact info for this contact"""
sub = (
Session.query(
Contact.id,
func.sum(case([(EmailLog.is_reply, 1)], else_=0)).label("nb_reply"),
func.sum(
case(
[
(
and_(
EmailLog.is_reply.is_(False),
EmailLog.blocked.is_(False),
),
1,
)
],
else_=0,
)
).label("nb_forward"),
func.max(EmailLog.created_at).label("max_email_log_created_at"),
)
.join(
EmailLog,
EmailLog.contact_id == Contact.id,
isouter=True,
)
.filter(Contact.alias_id == alias.id)
.group_by(Contact.id)
.subquery()
)
q = (
Session.query(
Contact,
EmailLog,
sub.c.nb_reply,
sub.c.nb_forward,
)
.join(
EmailLog,
EmailLog.contact_id == Contact.id,
isouter=True,
)
.filter(Contact.alias_id == alias.id)
.filter(Contact.id == sub.c.id)
.filter(
or_(
EmailLog.created_at == sub.c.max_email_log_created_at,
# no email log yet for this contact
sub.c.max_email_log_created_at.is_(None),
)
)
)
if query:
q = q.filter(
or_(
Contact.website_email.ilike(f"%{query}%"),
Contact.name.ilike(f"%{query}%"),
)
)
if contact_id:
q = q.filter(Contact.id == contact_id)
latest_activity = case(
[
(EmailLog.created_at > Contact.created_at, EmailLog.created_at),
(EmailLog.created_at < Contact.created_at, Contact.created_at),
],
else_=Contact.created_at,
)
q = (
q.order_by(latest_activity.desc())
.limit(config.PAGE_LIMIT)
.offset(page * config.PAGE_LIMIT)
)
ret = []
for contact, latest_email_log, nb_reply, nb_forward in q:
contact_info = ContactInfo(
contact=contact,
nb_forward=nb_forward,
nb_reply=nb_reply,
latest_email_log=latest_email_log,
)
ret.append(contact_info)
return ret
def delete_contact(alias: Alias, contact_id: int):
contact = Contact.get(contact_id)
if not contact:
flash("Unknown error. Refresh the page", "warning")
elif contact.alias_id != alias.id:
flash("You cannot delete reverse-alias", "warning")
else:
delete_contact_email = contact.website_email
Contact.delete(contact_id)
Session.commit()
flash(f"Reverse-alias for {delete_contact_email} has been deleted", "success")
@dashboard_bp.route("/alias_contact_manager/<int:alias_id>/", methods=["GET", "POST"])
@login_required @login_required
def alias_contact_manager(alias_id, forward_email_id=None): @parallel_limiter.lock(name="contact_creation")
gen_email = GenEmail.get(alias_id) def alias_contact_manager(alias_id):
highlight_contact_id = None
if request.args.get("highlight_contact_id"):
try:
highlight_contact_id = int(request.args.get("highlight_contact_id"))
except ValueError:
flash("Invalid contact id", "error")
return redirect(url_for("dashboard.index"))
alias = Alias.get(alias_id)
page = 0
if request.args.get("page"):
page = int(request.args.get("page"))
query = request.args.get("query") or ""
# sanity check # sanity check
if not gen_email: if not alias:
flash("You do not have access to this page", "warning") flash("You do not have access to this page", "warning")
return redirect(url_for("dashboard.index")) return redirect(url_for("dashboard.index"))
if gen_email.user_id != current_user.id: if alias.user_id != current_user.id:
flash("You do not have access to this page", "warning") flash("You do not have access to this page", "warning")
return redirect(url_for("dashboard.index")) return redirect(url_for("dashboard.index"))
new_contact_form = NewContactForm() new_contact_form = NewContactForm()
csrf_form = CSRFValidationForm()
if request.method == "POST": if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if request.form.get("form-name") == "create": if request.form.get("form-name") == "create":
if new_contact_form.validate(): if new_contact_form.validate():
contact_email = new_contact_form.email.data.strip() contact_address = new_contact_form.email.data.strip()
try:
# generate a reply_email, make sure it is unique contact = create_contact(current_user, alias, contact_address)
# not use while to avoid infinite loop except (
for _ in range(1000): ErrContactErrorUpgradeNeeded,
reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}" ErrAddressInvalid,
if not ForwardEmail.get_by(reply_email=reply_email): ErrContactAlreadyExists,
break CannotCreateContactForReverseAlias,
) as excp:
website_email = get_email_part(contact_email) flash(excp.error_for_user(), "error")
return redirect(request.url)
# already been added flash(f"Reverse alias for {contact_address} is created", "success")
if ForwardEmail.get_by(
gen_email_id=gen_email.id, website_email=website_email
):
flash(f"{website_email} is already added", "error")
return redirect(
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
)
forward_email = ForwardEmail.create(
gen_email_id=gen_email.id,
website_email=website_email,
website_from=contact_email,
reply_email=reply_email,
)
LOG.d("create reverse-alias for %s", contact_email)
db.session.commit()
flash(f"Reverse alias for {contact_email} is created", "success")
return redirect( return redirect(
url_for( url_for(
"dashboard.alias_contact_manager", "dashboard.alias_contact_manager",
alias_id=alias_id, alias_id=alias_id,
forward_email_id=forward_email.id, highlight_contact_id=contact.id,
) )
) )
elif request.form.get("form-name") == "delete": elif request.form.get("form-name") == "delete":
forward_email_id = request.form.get("forward-email-id") contact_id = request.form.get("contact-id")
forward_email = ForwardEmail.get(forward_email_id) delete_contact(alias, contact_id)
if not forward_email:
flash("Unknown error. Refresh the page", "warning")
return redirect(
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
)
elif forward_email.gen_email_id != gen_email.id:
flash("You cannot delete reverse-alias", "warning")
return redirect(
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
)
contact_name = forward_email.website_from
ForwardEmail.delete(forward_email_id)
db.session.commit()
flash(f"Reverse-alias for {contact_name} has been deleted", "success")
return redirect( return redirect(
url_for("dashboard.alias_contact_manager", alias_id=alias_id) url_for("dashboard.alias_contact_manager", alias_id=alias_id)
) )
# make sure highlighted forward_email is at array start elif request.form.get("form-name") == "search":
forward_emails = gen_email.forward_emails query = request.form.get("query")
return redirect(
url_for(
"dashboard.alias_contact_manager",
alias_id=alias_id,
query=query,
highlight_contact_id=highlight_contact_id,
)
)
if forward_email_id: contact_infos = get_contact_infos(alias, page, query=query)
forward_emails = sorted( last_page = len(contact_infos) < config.PAGE_LIMIT
forward_emails, key=lambda fe: fe.id == forward_email_id, reverse=True nb_contact = Contact.filter(Contact.alias_id == alias.id).count()
# if highlighted contact isn't included, fetch it
# make sure highlighted contact is at array start
contact_ids = [contact_info.contact.id for contact_info in contact_infos]
if highlight_contact_id and highlight_contact_id not in contact_ids:
contact_infos = (
get_contact_infos(alias, contact_id=highlight_contact_id, query=query)
+ contact_infos
) )
return render_template( return render_template(
"dashboard/alias_contact_manager.html", "dashboard/alias_contact_manager.html",
forward_emails=forward_emails, contact_infos=contact_infos,
alias=gen_email.email, alias=alias,
gen_email=gen_email,
new_contact_form=new_contact_form, new_contact_form=new_contact_form,
forward_email_id=forward_email_id, highlight_contact_id=highlight_contact_id,
page=page,
last_page=last_page,
query=query,
nb_contact=nb_contact,
can_create_contacts=current_user.can_create_contacts(),
csrf_form=csrf_form,
) )

View File

@ -0,0 +1,13 @@
from app.dashboard.base import dashboard_bp
from flask_login import login_required, current_user
from app.alias_utils import alias_export_csv
from app.dashboard.views.enter_sudo import sudo_required
from app.extensions import limiter
@dashboard_bp.route("/alias_export", methods=["GET"])
@login_required
@sudo_required
@limiter.limit("2/minute")
def alias_export_route():
return alias_export_csv(current_user)

View File

@ -4,19 +4,20 @@ from flask_login import login_required, current_user
from app.config import PAGE_LIMIT from app.config import PAGE_LIMIT
from app.dashboard.base import dashboard_bp from app.dashboard.base import dashboard_bp
from app.extensions import db from app.db import Session
from app.models import GenEmail, ForwardEmailLog, ForwardEmail from app.models import Alias, EmailLog, Contact
class AliasLog: class AliasLog:
website_email: str website_email: str
website_from: str reverse_alias: str
alias: str alias: str
when: arrow.Arrow when: arrow.Arrow
is_reply: bool is_reply: bool
blocked: bool blocked: bool
bounced: bool bounced: bool
mailbox: str email_log: EmailLog
contact: Contact
def __init__(self, **kwargs): def __init__(self, **kwargs):
for k, v in kwargs.items(): for k, v in kwargs.items():
@ -29,31 +30,31 @@ class AliasLog:
@dashboard_bp.route("/alias_log/<int:alias_id>/<int:page_id>") @dashboard_bp.route("/alias_log/<int:alias_id>/<int:page_id>")
@login_required @login_required
def alias_log(alias_id, page_id): def alias_log(alias_id, page_id):
gen_email = GenEmail.get(alias_id) alias = Alias.get(alias_id)
# sanity check # sanity check
if not gen_email: if not alias:
flash("You do not have access to this page", "warning") flash("You do not have access to this page", "warning")
return redirect(url_for("dashboard.index")) return redirect(url_for("dashboard.index"))
if gen_email.user_id != current_user.id: if alias.user_id != current_user.id:
flash("You do not have access to this page", "warning") flash("You do not have access to this page", "warning")
return redirect(url_for("dashboard.index")) return redirect(url_for("dashboard.index"))
logs = get_alias_log(gen_email, page_id) logs = get_alias_log(alias, page_id)
base = ( base = (
db.session.query(ForwardEmail, ForwardEmailLog) Session.query(Contact, EmailLog)
.filter(ForwardEmail.id == ForwardEmailLog.forward_id) .filter(Contact.id == EmailLog.contact_id)
.filter(ForwardEmail.gen_email_id == gen_email.id) .filter(Contact.alias_id == alias.id)
) )
total = base.count() total = base.count()
email_forwarded = ( email_forwarded = (
base.filter(ForwardEmailLog.is_reply == False) base.filter(EmailLog.is_reply.is_(False))
.filter(ForwardEmailLog.blocked == False) .filter(EmailLog.blocked.is_(False))
.count() .count()
) )
email_replied = base.filter(ForwardEmailLog.is_reply == True).count() email_replied = base.filter(EmailLog.is_reply.is_(True)).count()
email_blocked = base.filter(ForwardEmailLog.blocked == True).count() email_blocked = base.filter(EmailLog.blocked.is_(True)).count()
last_page = ( last_page = (
len(logs) < PAGE_LIMIT len(logs) < PAGE_LIMIT
) # lightweight pagination without counting all objects ) # lightweight pagination without counting all objects
@ -61,31 +62,31 @@ def alias_log(alias_id, page_id):
return render_template("dashboard/alias_log.html", **locals()) return render_template("dashboard/alias_log.html", **locals())
def get_alias_log(gen_email: GenEmail, page_id=0): def get_alias_log(alias: Alias, page_id=0) -> [AliasLog]:
logs: [AliasLog] = [] logs: [AliasLog] = []
mailbox = gen_email.mailbox_email()
q = ( q = (
db.session.query(ForwardEmail, ForwardEmailLog) Session.query(Contact, EmailLog)
.filter(ForwardEmail.id == ForwardEmailLog.forward_id) .filter(Contact.id == EmailLog.contact_id)
.filter(ForwardEmail.gen_email_id == gen_email.id) .filter(Contact.alias_id == alias.id)
.order_by(ForwardEmailLog.id.desc()) .order_by(EmailLog.id.desc())
.limit(PAGE_LIMIT) .limit(PAGE_LIMIT)
.offset(page_id * PAGE_LIMIT) .offset(page_id * PAGE_LIMIT)
) )
for fe, fel in q: for contact, email_log in q:
al = AliasLog( al = AliasLog(
website_email=fe.website_email, website_email=contact.website_email,
website_from=fe.website_from, reverse_alias=contact.website_send_to(),
alias=gen_email.email, alias=alias.email,
when=fel.created_at, when=email_log.created_at,
is_reply=fel.is_reply, is_reply=email_log.is_reply,
blocked=fel.blocked, blocked=email_log.blocked,
bounced=fel.bounced, bounced=email_log.bounced,
mailbox=mailbox, email_log=email_log,
contact=contact,
) )
logs.append(al) logs.append(al)
logs = sorted(logs, key=lambda l: l.when, reverse=True) logs = sorted(logs, key=lambda log: log.when, reverse=True)
return logs return logs

Some files were not shown because too many files have changed in this diff Show More