Compare commits

...

3683 Commits

Author SHA1 Message Date
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
850 changed files with 78380 additions and 378926 deletions

View File

@ -6,5 +6,12 @@ db.sqlite
.vscode
.DS_Store
config
LICENSE
README.md
adhoc
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:
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
strategy:
max-parallel: 4
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:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Check out repo
uses: actions/checkout@v3
- name: Test formatting
run: |
pip install black
black --check .
- name: Install poetry
run: pipx install poetry
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
- name: Test with pytest
run: |
pip install pytest
pytest
- name: Install OS dependencies
if: ${{ matrix.python-version }} == '3.10'
run: |
sudo apt update
sudo apt install -y libre2-dev libpq-dev
- name: Publish to Docker Registry
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: simplelogin/app-ci
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction
- 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 }}
- name: Start Redis v6
uses: superchargejs/redis-github-action@1.1.0
with:
redis-version: 6
- 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@v2
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 }}

10
.gitignore vendored
View File

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

3
.jshintrc Normal file
View File

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

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

@ -0,0 +1,25 @@
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.3.0
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,7 +6,94 @@ The version corresponds to SimpleLogin Docker `image tag`.
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).
## [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
## [2.0.0] - 2020-03-13
@ -30,7 +117,7 @@ Add SUPPORT_NAME param to set a support email name.
## [1.0.1] - 2020-01-28
Simplify config file.
Simplify config file.
## [1.0.0] - 2020-01-22

228
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,228 @@
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.7+ and [poetry](https://python-poetry.org/) to manage dependencies
- Node v10 for front-end.
- Postgres 12+
First, install all dependencies by running the following command.
Feel free to use `virtualenv` or similar tools to isolate development environment.
```bash
poetry install
```
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
```

View File

@ -2,15 +2,38 @@
FROM node:10.17.0-alpine AS npm
WORKDIR /code
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
# install dependencies
COPY ./requirements.txt ./
RUN pip3 install --no-cache-dir -r requirements.txt
# Copy poetry files
COPY poetry.lock pyproject.toml ./
# 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 --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
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:
Preamble
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The GNU Affero General Public License is a free, copyleft license for
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
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
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>
<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">
</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">
</a>
<a href="https://stats.uptimerobot.com/APkzziNWoM">
<img src="https://img.shields.io/uptimerobot/ratio/7/m782965045-15d8e413b20b5376f58db050">
</a>
<a href="./LICENSE">
<img src="https://img.shields.io/github/license/simple-login/app">
</a>
<a href="https://twitter.com/simple_login">
<img src="https://img.shields.io/twitter/follow/simple_login?style=social">
<a href="https://twitter.com/simplelogin">
<img src="https://img.shields.io/twitter/follow/simplelogin?style=social">
</a>
</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">
<img src="./docs/custom-alias.png" height="150px">
<a href="https://simplelogin.io">
<img src="./docs/hero.png" height="600px">
</a>
</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
docker run --name sl -it --rm \
-e RESET_DB=true \
-e CONFIG=/code/example.env \
-p 7777:7777 \
simplelogin/app:2.0.0 python server.py
```
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.
Then open http://localhost:7777, you should be able to login with `john@wick.com/password` account!
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
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.
### 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.
- [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.
### 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:
```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
@ -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:
```bash
openssl genrsa -out dkim.key 1024
openssl genrsa -out dkim.key -traditional 1024
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
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
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:
```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
@ -208,7 +161,7 @@ Similar to DKIM, setting up SPF is highly recommended.
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.
@ -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.
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 might need to exit and ssh again to your server for this to be taken into account.
You can also install Docker using the [docker-install](https://github.com/docker/docker-install) script which is
```bash
sudo usermod -a -G docker $USER
curl -fsSL https://get.docker.com | sh
```
### Prepare the Docker network
@ -265,8 +217,8 @@ Later, we will setup Postfix to authorize this network.
```bash
sudo docker network create -d bridge \
--subnet=240.0.0.0/24 \
--gateway=240.0.0.1 \
--subnet=10.0.0.0/24 \
--gateway=10.0.0.1 \
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.
```bash
sudo docker run -d \
docker run -d \
--name sl-db \
-e POSTGRES_PASSWORD=mypassword \
-e POSTGRES_USER=myuser \
-e POSTGRES_DB=simplelogin \
-p 5432:5432 \
-p 127.0.0.1:5432:5432 \
-v $(pwd)/sl/db:/var/lib/postgresql/data \
--restart always \
--network="sl-network" \
postgres:12.1
@ -293,7 +246,7 @@ sudo docker run -d \
To test whether the database operates correctly or not, run the following command:
```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.
@ -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.
![](./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.
```
@ -330,17 +286,19 @@ compatibility_level = 2
# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
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
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
# information on enabling SSL in the smtp client.
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
mydestination =
myhostname = app.mydomain.com
mydomain = mydomain.com
myorigin = mydomain.com
@ -376,8 +334,14 @@ smtpd_recipient_restrictions =
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.
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
@ -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.
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
@ -411,14 +375,16 @@ Finally, restart Postfix
sudo systemctl restart postfix
```
### 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
# 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.
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
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_URI=postgresql://myuser:mypassword@sl-db:5432/simplelogin
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:
```bash
sudo docker run --rm \
docker run --rm \
--name sl-migration \
-v $(pwd)/sl:/sl \
-v $(pwd)/sl/upload:/code/static/upload \
-v $(pwd)/dkim.key:/dkim.key \
-v $(pwd)/dkim.pub.key:/dkim.pub.key \
-v $(pwd)/simplelogin.env:/code/.env \
--network="sl-network" \
simplelogin/app:2.0.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.
Now, it's time to run the `webapp` container!
Init data
```bash
sudo docker run -d \
--name sl-app \
docker run --rm \
--name sl-init \
-v $(pwd)/sl:/sl \
-v $(pwd)/simplelogin.env:/code/.env \
-v $(pwd)/dkim.key:/dkim.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 \
--network="sl-network" \
simplelogin/app:2.0.0
simplelogin/app:3.4.0
```
Next run the `email handler`
```bash
sudo docker run -d \
docker run -d \
--name sl-email \
-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 20381:20381 \
-p 127.0.0.1:20381:20381 \
--restart always \
--network="sl-network" \
simplelogin/app:2.0.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
Install Nginx
Install Nginx and make sure to replace `mydomain.com` by your domain
```bash
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:
```
```nginx
server {
server_name app.mydomain.com;
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
```bash
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!
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,
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:
```
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 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
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:
- [Troubleshooting](docs/troubleshooting.md)
- [Enable SSL](docs/ssl.md)
- [UFW - uncomplicated firewall](docs/ufw.md)
- [SES - Amazon Simple Email Service](docs/ses.md)
- [Upgrade existing SimpleLogin installation](docs/upgrade.md)
## Contributing
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
```
- [Enforce SPF](docs/enforce-spf.md)
- [Postfix TLS](docs/postfix-tls.md)
## ❤️ 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/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://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>
</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,89 @@
from flask import redirect, url_for, request
from typing import Optional
import arrow
import sqlalchemy
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.actions import action
from flask_admin.contrib import sqla
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,
)
from app.newsletter_utils import send_newsletter_to_user, send_newsletter_to_address
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):
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
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):
@ -19,4 +92,582 @@ class SLAdminIndexView(AdminIndexView):
if not current_user.is_authenticated or not current_user.is_admin:
return redirect(url_for("auth.login", next=request.url))
return super(SLAdminIndexView, self).index()
return redirect("/admin/user")
def _user_upgrade_channel_formatter(view, context, model, name):
return Markup(model.upgrade_channel)
class UserAdmin(SLModelView):
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,
}
@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):
column_searchable_list = ["id"]
column_filters = ["id", "user.email", "mailbox.email", "contact.website_email"]
can_edit = False
can_create = False
class AliasAdmin(SLModelView):
column_searchable_list = ["id", "user.email", "email", "mailbox.email"]
column_filters = ["id", "user.email", "email", "mailbox.email"]
@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):
column_searchable_list = ["id", "user.email", "email"]
column_filters = ["id", "user.email", "email"]
# class LifetimeCouponAdmin(SLModelView):
# can_edit = True
# can_create = True
class CouponAdmin(SLModelView):
can_edit = False
can_create = True
class ManualSubscriptionAdmin(SLModelView):
can_edit = True
column_searchable_list = ["id", "user.email"]
@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):
column_searchable_list = ["domain", "user.email", "user.id"]
column_exclude_list = ["ownership_txt_token"]
can_edit = False
class ReferralAdmin(SLModelView):
column_searchable_list = ["id", "user.email", "code", "name"]
column_filters = ["id", "user.email", "code", "name"]
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
def _admin_action_formatter(view, context, model, name):
action_name = AuditLogActionEnum.get_name(model.action)
return "{} ({})".format(action_name, model.action)
def _admin_created_at_formatter(view, context, model, name):
return model.created_at.format()
class AdminAuditLogAdmin(SLModelView):
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_created_at_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):
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_created_at_formatter,
"updated_at": _admin_created_at_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):
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):
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):
column_exclude_list = ["created_at", "updated_at", "id"]
can_export = True
class MetricAdmin(SLModelView):
column_exclude_list = ["created_at", "updated_at", "id"]
can_export = True
class InvalidMailboxDomainAdmin(SLModelView):
can_create = True
can_delete = True

190
app/alias_suffix.py Normal file
View File

@ -0,0 +1,190 @@
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
if (
alias_domain in user.available_sl_domains(alias_options=alias_options)
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 user.available_sl_domains(
alias_options=alias_options
):
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

479
app/alias_utils.py Normal file
View File

@ -0,0 +1,479 @@
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, AliasStatusChange, EventContent
from app.log import LOG
from app.models import (
Alias,
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:
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):
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:
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:
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("no rule passed to create %s", local)
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
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:
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}")
if notify_user:
send_cannot_create_directory_alias(user, address, directory_name)
return None
if directory.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):
"""
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,
)
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)
Session.add(deleted_alias)
Session.commit()
LOG.i(f"Moving {alias} to global trash {deleted_alias}")
Alias.filter(Alias.id == alias.id).delete()
Session.commit()
EventDispatcher.send_event(
user, EventContent(alias_deleted=AliasDeleted(alias_id=alias.id))
)
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",
alias=alias,
),
render(
"transactional/alias-transferred.html",
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):
alias.enabled = enabled
event = AliasStatusChange(
alias_id=alias.id, alias_email=alias.email, enabled=enabled
)
EventDispatcher.send_event(alias.user, EventContent(alias_status_change=event))
if commit:
Session.commit()

View File

@ -1,9 +1,37 @@
from .views import (
alias_options,
new_custom_alias,
custom_domain,
new_random_alias,
user_info,
auth_login,
auth,
auth_mfa,
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,70 @@
from functools import wraps
from typing import Tuple, Optional
import arrow
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
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:
g.user = current_user
else:
return jsonify(error="Wrong api key"), 401
else:
# Update api key stats
api_key.last_used = arrow.now()
api_key.times += 1
db.session.commit()
Session.commit()
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 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,37 @@
from deprecated import deprecated
from flask import g
from flask import jsonify, request
from flask_cors import cross_origin
from flask import jsonify
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.index import get_alias_info, AliasInfo
from app.extensions import db
from app.models import GenEmail
from app.db import Session
from app.errors import (
CannotCreateContactForReverseAlias,
ErrContactErrorUpgradeNeeded,
ErrContactAlreadyExists,
ErrAddressInvalid,
)
from app.extensions import limiter
from app.models import Alias, Contact, Mailbox, AliasMailbox
@api_bp.route("/aliases")
@cross_origin()
@verify_api_key
@deprecated
@api_bp.route("/aliases", methods=["GET", "POST"])
@require_api_auth
@limiter.limit("10/minute", key_func=lambda: g.user.id)
def get_aliases():
"""
Get aliases
@ -35,32 +55,96 @@ def get_aliases():
except (ValueError, TypeError):
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 (
jsonify(
aliases=[
{
"id": alias.id,
"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,
"nb_block": alias.nb_blocked,
"nb_reply": alias.nb_reply,
"enabled": alias.gen_email.enabled,
"note": alias.note,
}
for alias in aliases
]
aliases=[serialize_alias_info(alias_info) for alias_info in alias_infos]
),
200,
)
@api_bp.route("/v2/aliases", methods=["GET", "POST"])
@require_api_auth
@limiter.limit("50/minute", key_func=lambda: g.user.id)
def get_aliases_v2():
"""
Get 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,
)
@api_bp.route("/aliases/<int:alias_id>", methods=["DELETE"])
@cross_origin()
@verify_api_key
@require_api_auth
def delete_alias(alias_id):
"""
Delete alias
@ -71,20 +155,18 @@ def delete_alias(alias_id):
"""
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
GenEmail.delete(alias_id)
db.session.commit()
alias_utils.delete_alias(alias, user)
return jsonify(deleted=True), 200
@api_bp.route("/aliases/<int:alias_id>/toggle", methods=["POST"])
@cross_origin()
@verify_api_key
@require_api_auth
def toggle_alias(alias_id):
"""
Enable/disable alias
@ -97,20 +179,19 @@ def toggle_alias(alias_id):
"""
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
gen_email.enabled = not gen_email.enabled
db.session.commit()
alias_utils.change_alias_status(alias, enabled=not 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")
@cross_origin()
@verify_api_key
@require_api_auth
def get_alias_activities(alias_id):
"""
Get aliases
@ -121,7 +202,8 @@ def get_alias_activities(alias_id):
- from
- to
- timestamp
- action: forward|reply|block
- action: forward|reply|block|bounced
- reverse_alias
"""
user = g.user
@ -130,23 +212,27 @@ def get_alias_activities(alias_id):
except (ValueError, TypeError):
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
alias_logs = get_alias_log(gen_email, page_id)
alias_logs = get_alias_log(alias, page_id)
activities = []
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:
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"
else:
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:
activity["action"] = "bounced"
@ -157,4 +243,235 @@ def get_alias_activities(alias_id):
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_cors import cross_origin
from sqlalchemy import desc
from app.api.base import api_bp, verify_api_key
from app.config import ALIAS_DOMAINS, DISABLE_ALIAS_SUFFIX
from app.extensions import db
from app.alias_suffix import get_alias_suffixes
from app.api.base import api_bp, require_api_auth
from app.db import Session
from app.log import LOG
from app.models import AliasUsedOn, GenEmail, User
from app.utils import convert_to_id, random_word
from app.models import AliasUsedOn, Alias, User
from app.utils import convert_to_id
@api_bp.route("/alias/options")
@cross_origin()
@verify_api_key
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():
@api_bp.route("/v4/alias/options")
@require_api_auth
def options_v4():
"""
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:
a valid api-key in "Authentication" header and
optional "hostname" in args
Output: cf README
can_create: bool
suffixes: [str]
suffixes: [[suffix, signed_suffix]]
prefix_suggestion: str
existing: [str]
recommendation: Optional dict
alias: str
hostname: str
@ -110,9 +33,6 @@ def options_v2():
hostname = request.args.get("hostname")
ret = {
"existing": [
ge.email for ge in GenEmail.query.filter_by(user_id=user.id, enabled=True)
],
"can_create": user.can_create_new_alias(),
"suffixes": [],
"prefix_suggestion": "",
@ -122,10 +42,10 @@ def options_v2():
if hostname:
# put the latest used alias first
q = (
db.session.query(AliasUsedOn, GenEmail, User)
Session.query(AliasUsedOn, Alias, User)
.filter(
AliasUsedOn.gen_email_id == GenEmail.id,
GenEmail.user_id == user.id,
AliasUsedOn.alias_id == Alias.id,
Alias.user_id == user.id,
AliasUsedOn.hostname == hostname,
)
.order_by(desc(AliasUsedOn.created_at))
@ -141,25 +61,93 @@ def options_v2():
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["prefix_suggestion"] = domain_name
ext = tldextract.extract(hostname)
prefix_suggestion = ext.domain
prefix_suggestion = convert_to_id(prefix_suggestion)
ret["prefix_suggestion"] = prefix_suggestion
# 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["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)
suffixes = get_alias_suffixes(user)
# 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)

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 google.oauth2.credentials
import googleapiclient.discovery
from flask import jsonify, request
from flask_cors import cross_origin
from flask_login import login_user
from itsdangerous import Signer
from app import email_utils
from app.api.base import api_bp
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 (
can_be_used_as_personal_email,
email_already_used,
email_can_be_used_as_mailbox,
personal_email_already_used,
send_email,
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.models import User, ApiKey, SocialAuth, AccountActivation
from app.utils import sanitize_email, canonicalize_email
@api_bp.route("/auth/login", methods=["POST"])
@cross_origin()
@limiter.limit("10/minute")
def auth_login():
"""
Authenticate user
@ -44,22 +49,39 @@ def auth_login():
if not data:
return jsonify(error="request body cannot be empty"), 400
email = data.get("email")
password = data.get("password")
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):
LoginEvent(LoginEvent.ActionType.failed, LoginEvent.Source.api).send()
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:
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
@api_bp.route("/auth/register", methods=["POST"])
@cross_origin()
@limiter.limit("10/minute")
def auth_register():
"""
User signs up - will need to activate their account with an activation code.
@ -74,38 +96,49 @@ def auth_register():
if not data:
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")
if DISABLE_REGISTRATION:
RegisterEvent(RegisterEvent.ActionType.failed, RegisterEvent.Source.api).send()
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
if not password or len(password) < 8:
RegisterEvent(RegisterEvent.ActionType.failed, RegisterEvent.Source.api).send()
return jsonify(error="password too short"), 400
LOG.debug("create user %s", email)
user = User.create(email=email, name="", password=password)
db.session.flush()
if len(password) > 100:
RegisterEvent(RegisterEvent.ActionType.failed, RegisterEvent.Source.api).send()
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
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)
db.session.commit()
Session.commit()
send_email(
email,
f"Just one more step to join SimpleLogin",
render("transactional/code-activation.txt", code=code),
"Just one more step to join SimpleLogin",
render("transactional/code-activation.txt.jinja2", code=code),
render("transactional/code-activation.html", code=code),
)
RegisterEvent(RegisterEvent.ActionType.success, RegisterEvent.Source.api).send()
return jsonify(msg="User needs to confirm their account"), 200
@api_bp.route("/auth/activate", methods=["POST"])
@cross_origin()
@limiter.limit("10/minute")
def auth_activate():
"""
User enters the activation code to confirm their account.
@ -122,10 +155,11 @@ def auth_activate():
if not data:
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")
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
if not user or user.activated:
@ -138,25 +172,25 @@ def auth_activate():
if account_activation.code != code:
# decrement nb tries
account_activation.tries -= 1
db.session.commit()
Session.commit()
if account_activation.tries == 0:
AccountActivation.delete(account_activation.id)
db.session.commit()
Session.commit()
return jsonify(error="Too many wrong tries"), 410
return jsonify(error="Wrong email or code"), 400
LOG.debug("activate user %s", user)
LOG.d("activate user %s", user)
user.activated = True
AccountActivation.delete(account_activation.id)
db.session.commit()
Session.commit()
return jsonify(msg="Account is activated, user can login now"), 200
@api_bp.route("/auth/reactivate", methods=["POST"])
@cross_origin()
@limiter.limit("10/minute")
def auth_reactivate():
"""
User asks for another activation code
@ -170,8 +204,10 @@ def auth_reactivate():
if not data:
return jsonify(error="request body cannot be empty"), 400
email = data.get("email")
user = User.get_by(email=email)
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)
# do not use a different message to avoid exposing existing email
if not user or user.activated:
@ -180,17 +216,17 @@ def auth_reactivate():
account_activation = AccountActivation.get_by(user_id=user.id)
if account_activation:
AccountActivation.delete(account_activation.id)
db.session.commit()
Session.commit()
# 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)
db.session.commit()
Session.commit()
send_email(
email,
f"Just one more step to join SimpleLogin",
render("transactional/code-activation.txt", code=code),
"Just one more step to join SimpleLogin",
render("transactional/code-activation.txt.jinja2", code=code),
render("transactional/code-activation.html", code=code),
)
@ -198,7 +234,7 @@ def auth_reactivate():
@api_bp.route("/auth/facebook", methods=["POST"])
@cross_origin()
@limiter.limit("10/minute")
def auth_facebook():
"""
Authenticate user with Facebook
@ -224,33 +260,35 @@ def auth_facebook():
graph = facebook.GraphAPI(access_token=facebook_token)
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)
if not user:
if DISABLE_REGISTRATION:
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
LOG.d("create facebook user with %s", user_info)
user = User.create(email=email.lower(), name=user_info["name"], activated=True)
db.session.commit()
user = User.create(email=email, name=user_info["name"], activated=True)
Session.commit()
email_utils.send_welcome_email(user)
if not SocialAuth.get_by(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
@api_bp.route("/auth/google", methods=["POST"])
@cross_origin()
@limiter.limit("10/minute")
def auth_google():
"""
Authenticate user with Facebook
Authenticate user with Google
Input:
google_token: Google access token
device: to create an ApiKey associated with this device
@ -276,30 +314,32 @@ def auth_google():
build = googleapiclient.discovery.build("oauth2", "v2", credentials=cred)
user_info = build.userinfo().get().execute()
email = user_info.get("email")
email = sanitize_email(user_info.get("email"))
user = User.get_by(email=email)
if not user:
if DISABLE_REGISTRATION:
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
LOG.d("create Google user with %s", user_info)
user = User.create(email=email.lower(), name="", activated=True)
db.session.commit()
user = User.create(email=email, name="", activated=True)
Session.commit()
email_utils.send_welcome_email(user)
if not SocialAuth.get_by(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
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
if user.enable_otp:
@ -311,8 +351,38 @@ def auth_payload(user, device) -> dict:
if not api_key:
LOG.d("create new api key for %s and %s", user, device)
api_key = ApiKey.create(user.id, device)
db.session.commit()
Session.commit()
ret["mfa_key"] = None
ret["api_key"] = api_key.code
# so user is automatically logged in on the web
login_user(user)
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
from flask import jsonify, request
from flask_cors import cross_origin
from itsdangerous import Signer, BadSignature
from flask_login import login_user
from itsdangerous import Signer
from app.api.base import api_bp
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.models import User, ApiKey
@api_bp.route("/auth/mfa", methods=["POST"])
@cross_origin()
@limiter.limit("10/minute")
def auth_mfa():
"""
Validate the OTP Token
@ -23,7 +25,8 @@ def auth_mfa():
200 and user info containing:
{
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)
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
ret = {"name": user.name}
ret = {"name": user.name or "", "email": user.email}
api_key = ApiKey.get_by(user_id=user.id, name=device)
if not api_key:
LOG.d("create new api key for %s and %s", user, device)
api_key = ApiKey.create(user.id, device)
db.session.commit()
Session.commit()
ret["api_key"] = api_key.code
# so user is logged in automatically on the web
login_user(user)
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)

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

@ -0,0 +1,236 @@
from smtplib import SMTPRecipientsRefused
import arrow
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 JOB_DELETE_MAILBOX
from app.dashboard.views.mailbox import send_verification_email
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.email_validation import is_valid_email
from app.log import LOG
from app.models import Mailbox, Job
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"))
if not user.is_premium():
return jsonify(error="Only premium plan can add additional mailbox"), 400
if not is_valid_email(mailbox_email):
return jsonify(error=f"{mailbox_email} invalid"), 400
elif mailbox_already_used(mailbox_email, user):
return jsonify(error=f"{mailbox_email} already used"), 400
elif not email_can_be_used_as_mailbox(mailbox_email):
return (
jsonify(
error=f"{mailbox_email} cannot be used. Please note a mailbox cannot "
f"be a disposable email address"
),
400,
)
else:
new_mailbox = Mailbox.create(email=mailbox_email, user_id=user.id)
Session.commit()
send_verification_email(user, new_mailbox)
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
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != user.id:
return jsonify(error="Forbidden"), 403
if mailbox.id == user.default_mailbox_id:
return jsonify(error="You cannot delete the default mailbox"), 400
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 = Mailbox.get(transfer_mailbox_id)
if not transfer_mailbox or transfer_mailbox.user_id != user.id:
return (
jsonify(error="You must transfer the aliases to a mailbox you own."),
403,
)
if transfer_mailbox_id == mailbox_id:
return (
jsonify(
error="You can not transfer the aliases to the mailbox you want to delete."
),
400,
)
if not transfer_mailbox.verified:
return jsonify(error="Your new mailbox is not verified"), 400
# Schedule delete account job
LOG.w("schedule delete mailbox job for %s", mailbox)
Job.create(
name=JOB_DELETE_MAILBOX,
payload={
"mailbox_id": mailbox.id,
"transfer_mailbox_id": transfer_mailbox_id,
},
run_at=arrow.now(),
commit=True,
)
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 jsonify, request
from flask_cors import cross_origin
from app.api.base import api_bp, verify_api_key
from app.config import MAX_NB_EMAIL_FREE_PLAN
from app.dashboard.views.custom_alias import verify_prefix_suffix
from app.extensions import db
from app import parallel_limiter
from app.alias_suffix import check_suffix_signature, verify_prefix_suffix
from app.alias_utils import check_alias_prefix
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.models import GenEmail, AliasUsedOn, User
from app.models import (
Alias,
AliasUsedOn,
User,
DeletedAlias,
DomainDeletedAlias,
Mailbox,
AliasMailbox,
)
from app.utils import convert_to_id
@api_bp.route("/alias/custom/new", methods=["POST"])
@cross_origin()
@verify_api_key
def new_custom_alias():
@api_bp.route("/v2/alias/custom/new", methods=["POST"])
@limiter.limit(ALIAS_LIMIT)
@require_api_auth
@parallel_limiter.lock(name="alias_creation")
def new_custom_alias_v2():
"""
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:
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 "note"
Output:
@ -38,33 +55,181 @@ def new_custom_alias():
400,
)
user_custom_domains = [cd.domain for cd in user.verified_custom_domains()]
hostname = request.args.get("hostname")
data = request.get_json()
if not data:
return jsonify(error="request body cannot be empty"), 400
alias_prefix = data.get("alias_prefix", "").strip()
alias_suffix = data.get("alias_suffix", "").strip()
alias_prefix = data.get("alias_prefix", "").strip().lower().replace(" ", "")
signed_suffix = data.get("signed_suffix", "").strip()
note = data.get("note")
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
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)
return jsonify(error=f"alias {full_alias} already exists"), 409
gen_email = GenEmail.create(
user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note
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,
mailbox_id=user.default_mailbox_id,
note=note,
)
db.session.commit()
Session.commit()
if hostname:
AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname)
db.session.commit()
AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id)
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 jsonify, request
from flask_cors import cross_origin
from app.api.base import api_bp, verify_api_key
from app.config import MAX_NB_EMAIL_FREE_PLAN
from app.extensions import db
from app import parallel_limiter
from app.alias_suffix import get_alias_suffixes
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.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"])
@cross_origin()
@verify_api_key
@limiter.limit(ALIAS_LIMIT)
@require_api_auth
@parallel_limiter.lock(name="alias_creation")
def new_random_alias():
"""
Create a new random alias
@ -37,22 +47,71 @@ def new_random_alias():
if data:
note = data.get("note")
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 alias"), 400
gen_email = GenEmail.create_new_random(user=user, scheme=scheme, note=note)
db.session.commit()
alias = None
# custom alias suggestion and suffix
hostname = request.args.get("hostname")
if hostname:
AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname)
db.session.commit()
if hostname and user.include_website_in_one_click_alias:
LOG.d("Use %s to create new alias", 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)
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,161 @@
from flask import jsonify, request, g
from flask_cors import cross_origin
from sqlalchemy import desc
import base64
import dataclasses
from io import BytesIO
from typing import Optional
from app.api.base import api_bp, verify_api_key
from app.config import EMAIL_DOMAIN
from app.extensions import db
from app.log import LOG
from app.models import AliasUsedOn, GenEmail, User
from app.utils import convert_to_id, random_word
from flask import jsonify, g, request, make_response
from app import s3, config
from app.api.base import api_bp, require_api_auth
from app.config import SESSION_COOKIE_NAME
from app.dashboard.views.index import get_stats
from app.db import Session
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")
@cross_origin()
@verify_api_key
@require_api_auth
def user_info():
"""
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
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 data["profile_picture"] is None:
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())
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,
google,
facebook,
proton,
change_email,
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 app import email_utils
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.models import ActivationCode
from app.utils import sanitize_next_url
@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():
if current_user.is_authenticated:
return (
@ -21,6 +26,8 @@ def activate():
activation_code: ActivationCode = ActivationCode.get_by(code=code)
if not activation_code:
# Trigger rate limiter
g.deduct_limit = True
return (
render_template(
"auth/activate.html", error="Activation code cannot be found"
@ -41,19 +48,22 @@ def activate():
user = activation_code.user
user.activated = True
login_user(user)
email_utils.send_welcome_email(user)
# activation code is to be used only once
ActivationCode.delete(activation_code.id)
db.session.commit()
Session.commit()
flash("Your account has been activated", "success")
email_utils.send_welcome_email(user)
# The activation link contains the original page, for ex authorize page
if "next" in request.args:
next_url = request.args.get("next")
LOG.debug("redirect user to %s", next_url)
next_url = sanitize_next_url(request.args.get("next"))
LOG.d("redirect user to %s", next_url)
return redirect(next_url)
else:
LOG.debug("redirect user to dashboard")
LOG.d("redirect user to dashboard")
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 app.auth.base import auth_bp
from app.extensions import db
from app.models import EmailChange
from app.db import Session
from app.extensions import limiter
from app.log import LOG
from app.models import EmailChange, ResetPasswordCode
@auth_bp.route("/change_email", methods=["GET", "POST"])
@limiter.limit("3/hour")
def change_email():
code = request.args.get("code")
email_change: EmailChange = EmailChange.get_by(code=code)
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():
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
old_email = user.email
user.email = email_change.new_email
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")
login_user(user)

View File

@ -1,22 +1,19 @@
from flask import request, session, redirect, url_for, flash
from flask_login import login_user
from requests_oauthlib import OAuth2Session
from requests_oauthlib.compliance_fixes import facebook_compliance_fix
from app import email_utils
from app.auth.base import auth_bp
from app.auth.views.google import create_file_from_url
from app.config import (
URL,
FACEBOOK_CLIENT_ID,
FACEBOOK_CLIENT_SECRET,
DISABLE_REGISTRATION,
)
from app.extensions import db
from app.db import Session
from app.log import LOG
from app.models import User, SocialAuth
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"
_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
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
# we need to pass the next url by session
@ -65,7 +62,7 @@ def facebook_callback():
redirect_uri=_redirect_uri,
)
facebook = facebook_compliance_fix(facebook)
token = facebook.fetch_token(
facebook.fetch_token(
_token_url,
client_secret=FACEBOOK_CLIENT_SECRET,
authorization_response=request.url,
@ -95,6 +92,7 @@ def facebook_callback():
)
return redirect(url_for("auth.register"))
email = sanitize_email(email)
user = User.get_by(email=email)
picture_url = facebook_user_data.get("picture", {}).get("data", {}).get("url")
@ -102,47 +100,28 @@ def facebook_callback():
if user:
if picture_url and not user.profile_picture_id:
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
db.session.commit()
Session.commit()
# create user
else:
if DISABLE_REGISTRATION:
flash("Registration is closed", "error")
return redirect(url_for("auth.login"))
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
flash(
"Sorry you cannot sign up via Facebook, please use email/password sign-up instead",
"error",
)
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")
return redirect(url_for("auth.register"))
next_url = None
# The activation link contains the original page, for ex authorize page
if "facebook_next_url" in session:
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 :)
session.pop("facebook_next_url", None)
if not SocialAuth.get_by(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)

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 wtforms import StringField, validators
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.utils import sanitize_email, canonicalize_email
class ForgotPasswordForm(FlaskForm):
@ -12,19 +15,27 @@ class ForgotPasswordForm(FlaskForm):
@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():
form = ForgotPasswordForm(request.form)
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:
error = "No such user, are you sure the email is correct?"
return render_template("auth/forgot_password.html", form=form, error=error)
email = sanitize_email(form.email.data)
canonical_email = canonicalize_email(email)
user = User.get_by(email=email) or User.get_by(email=canonical_email)
send_reset_password_email(user)
return redirect(url_for("auth.forgot_password"))
if user:
LOG.d("Send forgot password email to %s", user)
send_reset_password_email(user)
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_login import login_user
from requests_oauthlib import OAuth2Session
from app import email_utils
from app.auth.base import auth_bp
from app.auth.views.login_utils import after_login
from app.config import GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, URL, DISABLE_REGISTRATION
from app.email_utils import can_be_used_as_personal_email, email_already_used
from app.extensions import db
from app.config import GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, URL
from app.db import Session
from app.log import LOG
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"
_token_url = "https://github.com/login/oauth/access_token"
@ -22,7 +19,7 @@ _redirect_uri = URL + "/auth/github/callback"
@auth_bp.route("/github/login")
def github_login():
next_url = request.args.get("next")
next_url = sanitize_next_url(request.args.get("next"))
if next_url:
redirect_uri = _redirect_uri + "?next=" + encode_url(next_url)
else:
@ -51,7 +48,7 @@ def github_callback():
scope=["user:email"],
redirect_uri=_redirect_uri,
)
token = github.fetch_token(
github.fetch_token(
_token_url,
client_secret=GITHUB_CLIENT_SECRET,
authorization_response=request.url,
@ -78,41 +75,28 @@ def github_callback():
break
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(
"Cannot get a valid email from Github, please another way to login/sign up",
"error",
)
return redirect(url_for("auth.login"))
email = email.lower()
email = sanitize_email(email)
user = User.get_by(email=email)
# create user
if not user:
if DISABLE_REGISTRATION:
flash("Registration is closed", "error")
return redirect(url_for("auth.login"))
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
flash(
"Sorry you cannot sign up via Github, please use email/password sign-up instead",
"error",
)
db.session.commit()
login_user(user)
email_utils.send_welcome_email(user)
flash(f"Welcome to SimpleLogin {user.name}!", "success")
return redirect(url_for("auth.register"))
if not SocialAuth.get_by(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
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)

View File

@ -1,16 +1,14 @@
from flask import request, session, redirect, flash, url_for
from flask_login import login_user
from requests_oauthlib import OAuth2Session
from app import s3, email_utils
from app import s3
from app.auth.base import auth_bp
from app.config import URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, DISABLE_REGISTRATION
from app.extensions import db
from app.config import URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
from app.db import Session
from app.log import LOG
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 ...email_utils import can_be_used_as_personal_email, email_already_used
_authorization_base_url = "https://accounts.google.com/o/oauth2/v2/auth"
_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
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
# we need to pass the next url by session
@ -55,11 +53,12 @@ def google_callback():
google = OAuth2Session(
GOOGLE_CLIENT_ID,
state=session["oauth_state"],
# some how Google Login fails with oauth_state KeyError
# state=session["oauth_state"],
scope=_scope,
redirect_uri=_redirect_uri,
)
token = google.fetch_token(
google.fetch_token(
_token_url,
client_secret=GOOGLE_CLIENT_SECRET,
authorization_response=request.url,
@ -80,7 +79,7 @@ def google_callback():
"https://www.googleapis.com/oauth2/v1/userinfo"
).json()
email = google_user_data["email"]
email = sanitize_email(google_user_data["email"])
user = User.get_by(email=email)
picture_url = google_user_data.get("picture")
@ -88,58 +87,39 @@ def google_callback():
if user:
if picture_url and not user.profile_picture_id:
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
db.session.commit()
# create user
Session.commit()
else:
if DISABLE_REGISTRATION:
flash("Registration is closed", "error")
return redirect(url_for("auth.login"))
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
flash(
"Sorry you cannot sign up via Google, please use email/password sign-up instead",
"error",
)
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")
return redirect(url_for("auth.register"))
next_url = None
# The activation link contains the original page, for ex authorize page
if "google_next_url" in session:
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 :)
session.pop("google_next_url", None)
if not SocialAuth.get_by(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)
def create_file_from_url(url) -> File:
def create_file_from_url(user, url) -> File:
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)
db.session.flush()
Session.flush()
LOG.d("upload file %s to s3", 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_wtf import FlaskForm
from wtforms import StringField, validators
from app.auth.base import auth_bp
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.models import User
from app.utils import sanitize_email, sanitize_next_url, canonicalize_email
class LoginForm(FlaskForm):
@ -15,29 +19,56 @@ class LoginForm(FlaskForm):
@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():
next_url = sanitize_next_url(request.args.get("next"))
if current_user.is_authenticated:
LOG.d("user is already authenticated, redirect to dashboard")
return redirect(url_for("dashboard.index"))
if next_url:
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)
next_url = request.args.get("next")
show_resend_activation = False
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:
flash("Email or password incorrect", "error")
elif not user.check_password(form.password.data):
if not user or not user.check_password(form.password.data):
# Trigger rate limiter
g.deduct_limit = True
form.password.data = None
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:
show_resend_activation = True
flash(
"Please check your inbox for the activation email. You can also have this email re-sent",
"error",
)
LoginEvent(LoginEvent.ActionType.not_activated).send()
else:
LoginEvent(LoginEvent.ActionType.success).send()
return after_login(user, next_url)
return render_template(
@ -45,4 +76,7 @@ def login():
form=form,
next_url=next_url,
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 app.config import MFA_USER_ID
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.
If the user is logged in with Proton, do not look at fido nor otp
If user enables MFA: redirect user to MFA page
Otherwise redirect to dashboard page if no next_url
"""
if user.enable_otp:
session[MFA_USER_ID] = user.id
if next_url:
return redirect(url_for("auth.mfa", next_url=next_url))
else:
return redirect(url_for("auth.mfa"))
else:
LOG.debug("log user %s in", user)
login_user(user)
if not login_from_proton:
if user.fido_enabled():
# Use the same session for FIDO so that we can easily
# switch between these two 2FA option
session[MFA_USER_ID] = user.id
if next_url:
return redirect(url_for("auth.fido", next=next_url))
else:
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
if next_url:
LOG.debug("redirect user to %s", next_url)
return redirect(next_url)
else:
LOG.debug("redirect user to dashboard")
return redirect(url_for("dashboard.index"))
LOG.d("log user %s in", user)
login_user(user)
session["sudo_time"] = int(time())
# 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"))
# 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_login import logout_user
from flask import redirect, url_for, flash, make_response
from app.auth.base import auth_bp
from app.config import SESSION_COOKIE_NAME
from app.session import logout_session
@auth_bp.route("/logout")
def logout():
logout_user()
return render_template("auth/logout.html")
logout_session()
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
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_wtf import FlaskForm
from wtforms import StringField, validators
from wtforms import BooleanField, StringField, validators
from app.auth.base import auth_bp
from app.config import MFA_USER_ID
from app.log import LOG
from app.models import User
from app.config import MFA_USER_ID, URL
from app.db import Session
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):
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"])
@limiter.limit(
"10/minute", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
)
def mfa():
# passed from login page
user_id = session.get(MFA_USER_ID)
@ -31,28 +49,59 @@ def mfa():
return redirect(url_for("auth.login"))
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():
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]
user.last_otp = token
Session.commit()
login_user(user)
flash(f"Welcome back {user.name}!")
flash("Welcome back!", "success")
# User comes to login page from another page
if next_url:
LOG.debug("redirect user to %s", next_url)
return redirect(next_url)
else:
LOG.debug("redirect user to dashboard")
return redirect(url_for("dashboard.index"))
# Redirect user to correct page
response = make_response(redirect(next_url or url_for("dashboard.index")))
if otp_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
else:
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_login import current_user
from flask_wtf import FlaskForm
@ -5,18 +6,25 @@ from wtforms import StringField, validators
from app import email_utils, config
from app.auth.base import auth_bp
from app.config import URL, DISABLE_REGISTRATION
from app.email_utils import can_be_used_as_personal_email, email_already_used
from app.extensions import db
from app.config import CONNECT_WITH_PROTON, CONNECT_WITH_OIDC_ICON
from app.auth.views.login_utils import get_referral
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.models import User, ActivationCode
from app.utils import random_string, encode_url
from app.models import User, ActivationCode, DailyMetric
from app.utils import random_string, encode_url, sanitize_email, canonicalize_email
class RegisterForm(FlaskForm):
email = StringField("Email", validators=[validators.DataRequired()])
password = StringField(
"Password", validators=[validators.DataRequired(), validators.Length(min=8)]
"Password",
validators=[validators.DataRequired(), validators.Length(min=8, max=100)],
)
@ -35,28 +43,81 @@ def register():
next_url = request.args.get("next")
if form.validate_on_submit():
email = form.email.data.lower()
if not can_be_used_as_personal_email(email):
flash("You cannot use this email address as your personal inbox.", "error")
else:
if email_already_used(email):
flash(f"Email {email} already used", "error")
else:
LOG.debug("create user %s", form.email.data)
user = User.create(email=email, name="", password=form.password.data)
db.session.commit()
# only check if hcaptcha is enabled
if HCAPTCHA_SECRET:
# check with hCaptcha
token = request.form.get("h-captcha-response")
params = {"secret": HCAPTCHA_SECRET, "response": token}
hcaptcha_res = requests.post(
"https://hcaptcha.com/siteverify", data=params
).json()
# return something like
# {'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.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):
# the activation code is valid for 1h
activation = ActivationCode.create(user_id=user.id, code=random_string(30))
db.session.commit()
Session.commit()
# Send user activation email
activation_link = f"{URL}/auth/activate?code={activation.code}"
@ -64,4 +125,4 @@ def send_activation_email(user, next_url):
LOG.d("redirect user to %s after activation", 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.email, activation_link)

View File

@ -4,8 +4,10 @@ from wtforms import StringField, validators
from app.auth.base import auth_bp
from app.auth.views.register import send_activation_email
from app.extensions import limiter
from app.log import LOG
from app.models import User
from app.utils import sanitize_email, canonicalize_email
class ResendActivationForm(FlaskForm):
@ -13,11 +15,14 @@ class ResendActivationForm(FlaskForm):
@auth_bp.route("/resend_activation", methods=["GET", "POST"])
@limiter.limit("10/hour")
def resend_activation():
form = ResendActivationForm(request.form)
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:
flash("There is no such email", "warning")

View File

@ -1,21 +1,27 @@
import arrow
from flask import request, flash, render_template, redirect, url_for
from flask_login import login_user
import uuid
from flask import request, flash, render_template, url_for, g
from flask_wtf import FlaskForm
from wtforms import StringField, validators
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
class ResetPasswordForm(FlaskForm):
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"])
@limiter.limit(
"10/minute", deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit
)
def reset_password():
form = ResetPasswordForm(request.form)
@ -26,6 +32,8 @@ def reset_password():
)
if not reset_password_code:
# Trigger rate limiter
g.deduct_limit = True
error = (
"The reset password link can be used only once. "
"Please request a new link to reset password."
@ -38,20 +46,30 @@ def reset_password():
if form.validate_on_submit():
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")
# this can be served to activate user too
user.activated = True
# remove the reset password code
ResetPasswordCode.delete(reset_password_code.id)
# remove all reset password codes
ResetPasswordCode.filter_by(user_id=user.id).delete()
db.session.commit()
login_user(user)
# change the alternative_id to log user out on other browsers
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)

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 random
import socket
import string
import subprocess
import tempfile
from uuid import uuid4
from ast import literal_eval
from typing import Callable, List
from urllib.parse import urlparse
from dotenv import load_dotenv
SHA1 = subprocess.getoutput("git rev-parse HEAD")
ROOT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
@ -20,6 +20,21 @@ def get_abs_path(file_path: str):
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)
config_file = os.environ.get("CONFIG")
if config_file:
config_file = get_abs_path(config_file)
@ -28,18 +43,18 @@ if config_file:
else:
load_dotenv()
RESET_DB = "RESET_DB" 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
PROMO_CODE = "SIMPLEISBETTER"
# Debug mode
DEBUG = os.environ["DEBUG"] if "DEBUG" in os.environ else False
# Server url
URL = os.environ["URL"]
print(">>> URL:", URL)
# Calculate RP_ID for WebAuthn
RP_ID = urlparse(URL).hostname
SENTRY_DSN = os.environ.get("SENTRY_DSN")
# can use another sentry project for the front-end to avoid noises
@ -47,105 +62,151 @@ SENTRY_FRONT_END_DSN = os.environ.get("SENTRY_FRONT_END_DSN") or SENTRY_DSN
# Email related settings
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_NAME = os.environ.get("SUPPORT_NAME", "Son from SimpleLogin")
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:
MAX_NB_EMAIL_FREE_PLAN = int(os.environ["MAX_NB_EMAIL_FREE_PLAN"])
except Exception:
print("MAX_NB_EMAIL_FREE_PLAN is not set, use 5 as default value")
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")
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
# Useful when calling Postfix from an external network
POSTFIX_SUBMISSION_TLS = "POSTFIX_SUBMISSION_TLS" in os.environ
if "OTHER_ALIAS_DOMAINS" in os.environ:
OTHER_ALIAS_DOMAINS = eval(
os.environ["OTHER_ALIAS_DOMAINS"]
) # ["domain1.com", "domain2.com"]
if POSTFIX_SUBMISSION_TLS:
default_postfix_port = 587
else:
OTHER_ALIAS_DOMAINS = []
default_postfix_port = 25
POSTFIX_PORT = int(os.environ.get("POSTFIX_PORT", default_postfix_port))
POSTFIX_TIMEOUT = 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
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)
EMAIL_SERVERS_WITH_PRIORITY = eval(
os.environ["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 = []
# e.g. [(10, "mx1.hostname."), (10, "mx2.hostname.")]
EMAIL_SERVERS_WITH_PRIORITY = sl_getenv("EMAIL_SERVERS_WITH_PRIORITY")
# disable the alias suffix, i.e. the ".random_word" part
DISABLE_ALIAS_SUFFIX = "DISABLE_ALIAS_SUFFIX" in os.environ
DKIM_PRIVATE_KEY_PATH = get_abs_path(os.environ["DKIM_PRIVATE_KEY_PATH"])
DKIM_PUBLIC_KEY_PATH = get_abs_path(os.environ["DKIM_PUBLIC_KEY_PATH"])
# the email address that receives all unsubscription request
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_PRIVATE_KEY = None
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"]
if "DKIM_PRIVATE_KEY_PATH" in os.environ:
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()
# Database
DB_URI = os.environ["DB_URI"]
DB_CONN_NAME = os.environ.get("DB_CONN_NAME", "webapp")
# 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"
CUSTOM_ALIAS_SECRET = FLASK_SECRET + "custom_alias"
UNSUBSCRIBE_SECRET = FLASK_SECRET + "unsub"
# AWS
AWS_REGION = "eu-west-3"
AWS_REGION = os.environ.get("AWS_REGION") or "eu-west-3"
BUCKET = os.environ.get("BUCKET")
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
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"]
AWS_ENDPOINT_URL = os.environ.get("AWS_ENDPOINT_URL", None)
# Paddle
try:
PADDLE_VENDOR_ID = int(os.environ["PADDLE_VENDOR_ID"])
PADDLE_MONTHLY_PRODUCT_ID = int(os.environ["PADDLE_MONTHLY_PRODUCT_ID"])
PADDLE_YEARLY_PRODUCT_ID = int(os.environ["PADDLE_YEARLY_PRODUCT_ID"])
except:
except (KeyError, ValueError):
print("Paddle param not set")
PADDLE_VENDOR_ID = -1
PADDLE_MONTHLY_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(
os.environ.get("PADDLE_PUBLIC_KEY_PATH", "local_data/paddle.key.pub")
)
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_PRIVATE_KEY_PATH = get_abs_path(
os.environ.get("OPENID_PRIVATE_KEY_PATH", "local_data/jwtRS256.key")
@ -155,8 +216,10 @@ OPENID_PUBLIC_KEY_PATH = get_abs_path(
)
# 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(
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
@ -171,17 +234,33 @@ else:
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_SECRET = os.environ.get("GITHUB_CLIENT_SECRET")
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET")
FACEBOOK_CLIENT_ID = os.environ.get("FACEBOOK_CLIENT_ID")
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
AVATAR_URL_EXPIRATION = 3600 * 24 * 7 # 1h*24h/d*7d=1week
@ -191,9 +270,320 @@ MFA_USER_ID = "mfa_user_id"
FLASK_PROFILER_PATH = os.environ.get("FLASK_PROFILER_PATH")
FLASK_PROFILER_PASSWORD = os.environ.get("FLASK_PROFILER_PASSWORD")
# Job names
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"
# for pagination
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

View File

@ -0,0 +1,37 @@
from app.db import Session
from app.dns_utils import get_cname_record
from app.models import CustomDomain
class CustomDomainValidation:
def __init__(self, dkim_domain: str):
self.dkim_domain = dkim_domain
self._dkim_records = {
(f"{key}._domainkey", f"{key}._domainkey.{self.dkim_domain}")
for key in ("dkim", "dkim02", "dkim03")
}
def get_dkim_records(self) -> {str: str}:
"""
Get a list of dkim records to set up. It will be
"""
return self._dkim_records
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
"""
invalid_records = {}
for prefix, expected_record in self.get_dkim_records():
custom_record = f"{prefix}.{custom_domain.domain}"
dkim_record = get_cname_record(custom_record)
if dkim_record != expected_record:
invalid_records[custom_record] = dkim_record or "empty"
# HACK: If dkim is enabled, don't disable it to give users time to update their CNAMES
if custom_domain.dkim_verified:
return invalid_records
custom_domain.dkim_verified = len(invalid_records) == 0
Session.commit()
return invalid_records

View File

@ -3,18 +3,71 @@ from .views import (
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,
deleted_alias,
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.email, 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(email_change.new_email, user.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_wtf import FlaskForm
from sqlalchemy import and_, func, case
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.email_utils import get_email_part
from app.extensions import db
from app.db import Session
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.models import GenEmail, ForwardEmail
from app.utils import random_string
from app.models import Alias, Contact, EmailLog, User
from app.utils import sanitize_email, CSRFValidationForm
def email_validator():
@ -31,118 +45,280 @@ def email_validator():
if email.find("<") + 1 < email.find(">"):
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):
return
raise ValidationError(message)
if not is_valid_email(email_part):
raise ValidationError(message)
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):
email = StringField(
"Email", validators=[validators.DataRequired(), email_validator()]
)
@dashboard_bp.route("/alias_contact_manager/<alias_id>/", methods=["GET", "POST"])
@dashboard_bp.route(
"/alias_contact_manager/<alias_id>/<int:forward_email_id>", methods=["GET", "POST"]
)
@dataclass
class ContactInfo(object):
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
def alias_contact_manager(alias_id, forward_email_id=None):
gen_email = GenEmail.get(alias_id)
@parallel_limiter.lock(name="contact_creation")
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
if not gen_email:
if not alias:
flash("You do not have access to this page", "warning")
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")
return redirect(url_for("dashboard.index"))
new_contact_form = NewContactForm()
csrf_form = CSRFValidationForm()
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 new_contact_form.validate():
contact_email = new_contact_form.email.data.strip()
# generate a reply_email, make sure it is unique
# not use while to avoid infinite loop
for _ in range(1000):
reply_email = f"ra+{random_string(25)}@{EMAIL_DOMAIN}"
if not ForwardEmail.get_by(reply_email=reply_email):
break
website_email = get_email_part(contact_email)
# already been added
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")
contact_address = new_contact_form.email.data.strip()
try:
contact = create_contact(current_user, alias, contact_address)
except (
ErrContactErrorUpgradeNeeded,
ErrAddressInvalid,
ErrContactAlreadyExists,
CannotCreateContactForReverseAlias,
) as excp:
flash(excp.error_for_user(), "error")
return redirect(request.url)
flash(f"Reverse alias for {contact_address} is created", "success")
return redirect(
url_for(
"dashboard.alias_contact_manager",
alias_id=alias_id,
forward_email_id=forward_email.id,
highlight_contact_id=contact.id,
)
)
elif request.form.get("form-name") == "delete":
forward_email_id = request.form.get("forward-email-id")
forward_email = ForwardEmail.get(forward_email_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")
contact_id = request.form.get("contact-id")
delete_contact(alias, contact_id)
return redirect(
url_for("dashboard.alias_contact_manager", alias_id=alias_id)
)
# make sure highlighted forward_email is at array start
forward_emails = gen_email.forward_emails
elif request.form.get("form-name") == "search":
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:
forward_emails = sorted(
forward_emails, key=lambda fe: fe.id == forward_email_id, reverse=True
contact_infos = get_contact_infos(alias, page, query=query)
last_page = len(contact_infos) < config.PAGE_LIMIT
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(
"dashboard/alias_contact_manager.html",
forward_emails=forward_emails,
alias=gen_email.email,
gen_email=gen_email,
contact_infos=contact_infos,
alias=alias,
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.dashboard.base import dashboard_bp
from app.extensions import db
from app.models import GenEmail, ForwardEmailLog, ForwardEmail
from app.db import Session
from app.models import Alias, EmailLog, Contact
class AliasLog:
website_email: str
website_from: str
reverse_alias: str
alias: str
when: arrow.Arrow
is_reply: bool
blocked: bool
bounced: bool
mailbox: str
email_log: EmailLog
contact: Contact
def __init__(self, **kwargs):
for k, v in kwargs.items():
@ -29,31 +30,31 @@ class AliasLog:
@dashboard_bp.route("/alias_log/<int:alias_id>/<int:page_id>")
@login_required
def alias_log(alias_id, page_id):
gen_email = GenEmail.get(alias_id)
alias = Alias.get(alias_id)
# sanity check
if not gen_email:
if not alias:
flash("You do not have access to this page", "warning")
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")
return redirect(url_for("dashboard.index"))
logs = get_alias_log(gen_email, page_id)
logs = get_alias_log(alias, page_id)
base = (
db.session.query(ForwardEmail, ForwardEmailLog)
.filter(ForwardEmail.id == ForwardEmailLog.forward_id)
.filter(ForwardEmail.gen_email_id == gen_email.id)
Session.query(Contact, EmailLog)
.filter(Contact.id == EmailLog.contact_id)
.filter(Contact.alias_id == alias.id)
)
total = base.count()
email_forwarded = (
base.filter(ForwardEmailLog.is_reply == False)
.filter(ForwardEmailLog.blocked == False)
base.filter(EmailLog.is_reply.is_(False))
.filter(EmailLog.blocked.is_(False))
.count()
)
email_replied = base.filter(ForwardEmailLog.is_reply == True).count()
email_blocked = base.filter(ForwardEmailLog.blocked == True).count()
email_replied = base.filter(EmailLog.is_reply.is_(True)).count()
email_blocked = base.filter(EmailLog.blocked.is_(True)).count()
last_page = (
len(logs) < PAGE_LIMIT
) # 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())
def get_alias_log(gen_email: GenEmail, page_id=0):
def get_alias_log(alias: Alias, page_id=0) -> [AliasLog]:
logs: [AliasLog] = []
mailbox = gen_email.mailbox_email()
q = (
db.session.query(ForwardEmail, ForwardEmailLog)
.filter(ForwardEmail.id == ForwardEmailLog.forward_id)
.filter(ForwardEmail.gen_email_id == gen_email.id)
.order_by(ForwardEmailLog.id.desc())
Session.query(Contact, EmailLog)
.filter(Contact.id == EmailLog.contact_id)
.filter(Contact.alias_id == alias.id)
.order_by(EmailLog.id.desc())
.limit(PAGE_LIMIT)
.offset(page_id * PAGE_LIMIT)
)
for fe, fel in q:
for contact, email_log in q:
al = AliasLog(
website_email=fe.website_email,
website_from=fe.website_from,
alias=gen_email.email,
when=fel.created_at,
is_reply=fel.is_reply,
blocked=fel.blocked,
bounced=fel.bounced,
mailbox=mailbox,
website_email=contact.website_email,
reverse_alias=contact.website_send_to(),
alias=alias.email,
when=email_log.created_at,
is_reply=email_log.is_reply,
blocked=email_log.blocked,
bounced=email_log.bounced,
email_log=email_log,
contact=contact,
)
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

View File

@ -0,0 +1,171 @@
import base64
import hmac
import secrets
import arrow
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from app import config
from app.alias_utils import transfer_alias
from app.dashboard.base import dashboard_bp
from app.dashboard.views.enter_sudo import sudo_required
from app.db import Session
from app.extensions import limiter
from app.log import LOG
from app.models import (
Alias,
)
from app.models import Mailbox
from app.utils import CSRFValidationForm
def hmac_alias_transfer_token(transfer_token: str) -> str:
alias_hmac = hmac.new(
config.ALIAS_TRANSFER_TOKEN_SECRET.encode("utf-8"),
transfer_token.encode("utf-8"),
"sha3_224",
)
return base64.urlsafe_b64encode(alias_hmac.digest()).decode("utf-8").rstrip("=")
@dashboard_bp.route("/alias_transfer/send/<int:alias_id>/", methods=["GET", "POST"])
@login_required
@sudo_required
def alias_transfer_send_route(alias_id):
alias = Alias.get(alias_id)
if not alias or alias.user_id != current_user.id:
flash("You cannot see this page", "warning")
return redirect(url_for("dashboard.index"))
if current_user.newsletter_alias_id == alias.id:
flash(
"This alias is currently used for receiving the newsletter and cannot be transferred",
"error",
)
return redirect(url_for("dashboard.index"))
alias_transfer_url = None
csrf_form = CSRFValidationForm()
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
# generate a new transfer_token
if request.form.get("form-name") == "create":
transfer_token = f"{alias.id}.{secrets.token_urlsafe(32)}"
alias.transfer_token = hmac_alias_transfer_token(transfer_token)
alias.transfer_token_expiration = arrow.utcnow().shift(hours=24)
Session.commit()
alias_transfer_url = (
config.URL
+ "/dashboard/alias_transfer/receive"
+ f"?token={transfer_token}"
)
flash("Share alias URL created", "success")
# request.form.get("form-name") == "remove"
else:
alias.transfer_token = None
alias.transfer_token_expiration = None
Session.commit()
alias_transfer_url = None
flash("Share URL deleted", "success")
return render_template(
"dashboard/alias_transfer_send.html",
alias=alias,
alias_transfer_url=alias_transfer_url,
link_active=alias.transfer_token_expiration is not None
and alias.transfer_token_expiration > arrow.utcnow(),
csrf_form=csrf_form,
)
@dashboard_bp.route("/alias_transfer/receive", methods=["GET", "POST"])
@limiter.limit("5/minute")
@login_required
def alias_transfer_receive_route():
"""
URL has ?alias_id=signed_alias_id
"""
token = request.args.get("token")
if not token:
flash("Invalid transfer token", "error")
return redirect(url_for("dashboard.index"))
hashed_token = hmac_alias_transfer_token(token)
# TODO: Don't allow unhashed tokens once all the tokens have been migrated to the new format
alias = Alias.get_by(transfer_token=token) or Alias.get_by(
transfer_token=hashed_token
)
if not alias:
flash("Invalid link", "error")
return redirect(url_for("dashboard.index"))
# TODO: Don't allow none once all the tokens have been migrated to the new format
if (
alias.transfer_token_expiration is not None
and alias.transfer_token_expiration < arrow.utcnow()
):
flash("Expired link, please request a new one", "error")
return redirect(url_for("dashboard.index"))
# alias already belongs to this user
if alias.user_id == current_user.id:
flash("You already own this alias", "warning")
return redirect(url_for("dashboard.index"))
# check if user has not exceeded the alias quota
if not current_user.can_create_new_alias():
LOG.d("%s can't receive new alias", current_user)
flash(
"You have reached free plan limit, please upgrade to create new aliases",
"warning",
)
return redirect(url_for("dashboard.index"))
mailboxes = current_user.mailboxes()
if request.method == "POST":
mailbox_ids = request.form.getlist("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 != current_user.id
or not mailbox.verified
):
flash("Something went wrong, please retry", "warning")
return redirect(request.url)
mailboxes.append(mailbox)
if not mailboxes:
flash("You must select at least 1 mailbox", "warning")
return redirect(request.url)
LOG.d(
"transfer alias %s from %s to %s with %s with token %s",
alias,
alias.user,
current_user,
mailboxes,
token,
)
transfer_alias(alias, current_user, mailboxes)
# reset transfer token
alias.transfer_token = None
alias.transfer_token_expiration = None
Session.commit()
flash(f"You are now owner of {alias.email}", "success")
return redirect(url_for("dashboard.index", highlight_alias_id=alias.id))
return render_template(
"dashboard/alias_transfer_receive.html",
alias=alias,
mailboxes=mailboxes,
)

View File

@ -3,23 +3,61 @@ from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from wtforms import StringField, validators
from app import config
from app.dashboard.base import dashboard_bp
from app.extensions import db
from app.dashboard.views.enter_sudo import sudo_required
from app.db import Session
from app.extensions import limiter
from app.models import ApiKey
from app.utils import CSRFValidationForm
class NewApiKeyForm(FlaskForm):
name = StringField("Name", validators=[validators.DataRequired()])
def clean_up_unused_or_old_api_keys(user_id: int):
total_keys = ApiKey.filter_by(user_id=user_id).count()
if total_keys <= config.MAX_API_KEYS:
return
# Remove oldest unused
for api_key in (
ApiKey.filter_by(user_id=user_id, last_used=None)
.order_by(ApiKey.created_at.asc())
.all()
):
Session.delete(api_key)
total_keys -= 1
if total_keys <= config.MAX_API_KEYS:
return
# Clean up oldest used
for api_key in (
ApiKey.filter_by(user_id=user_id).order_by(ApiKey.last_used.asc()).all()
):
Session.delete(api_key)
total_keys -= 1
if total_keys <= config.MAX_API_KEYS:
return
@dashboard_bp.route("/api_key", methods=["GET", "POST"])
@login_required
@sudo_required
@limiter.limit("10/hour")
def api_key():
api_keys = ApiKey.query.filter(ApiKey.user_id == current_user.id).all()
api_keys = (
ApiKey.filter(ApiKey.user_id == current_user.id)
.order_by(ApiKey.created_at.desc())
.all()
)
csrf_form = CSRFValidationForm()
new_api_key_form = NewApiKeyForm()
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if request.form.get("form-name") == "delete":
api_key_id = request.form.get("api-key-id")
@ -34,23 +72,31 @@ def api_key():
name = api_key.name
ApiKey.delete(api_key_id)
db.session.commit()
Session.commit()
flash(f"API Key {name} has been deleted", "success")
return redirect(url_for("dashboard.api_key"))
elif request.form.get("form-name") == "create":
if new_api_key_form.validate():
clean_up_unused_or_old_api_keys(current_user.id)
new_api_key = ApiKey.create(
name=new_api_key_form.name.data, user_id=current_user.id
)
db.session.commit()
Session.commit()
flash(f"New API Key {new_api_key.name} has been created", "success")
return redirect(url_for("dashboard.api_key"))
return render_template(
"dashboard/new_api_key.html", api_key=new_api_key
)
elif request.form.get("form-name") == "delete-all":
ApiKey.delete_all(current_user.id)
Session.commit()
flash("All API Keys have been deleted", "success")
return redirect(url_for("dashboard.api_key"))
return render_template(
"dashboard/api_key.html", api_keys=api_keys, new_api_key_form=new_api_key_form
"dashboard/api_key.html",
api_keys=api_keys,
new_api_key_form=new_api_key_form,
csrf_form=csrf_form,
)

View File

@ -0,0 +1,47 @@
from flask import render_template, request, flash, redirect
from flask_login import login_required, current_user
from sqlalchemy.orm import joinedload
from app.dashboard.base import dashboard_bp
from app.db import Session
from app.models import (
ClientUser,
)
@dashboard_bp.route("/app", methods=["GET", "POST"])
@login_required
def app_route():
"""
List of apps that user has used via the "Sign in with SimpleLogin"
"""
client_users = (
ClientUser.filter_by(user_id=current_user.id)
.options(joinedload(ClientUser.client))
.options(joinedload(ClientUser.alias))
.all()
)
sorted(client_users, key=lambda cu: cu.client.name)
if request.method == "POST":
client_user_id = request.form.get("client-user-id")
client_user = ClientUser.get(client_user_id)
if not client_user or client_user.user_id != current_user.id:
flash(
"Unknown error, sorry for the inconvenience, refresh the page", "error"
)
return redirect(request.url)
client = client_user.client
ClientUser.delete(client_user_id)
Session.commit()
flash(f"Link with {client.name} has been removed", "success")
return redirect(request.url)
return render_template(
"dashboard/app.html",
client_users=client_users,
)

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