2020-02-03 07:11:11 +01:00
|
|
|
"""
|
|
|
|
Run scheduled jobs.
|
|
|
|
Not meant for running job at precise time (+- 1h)
|
|
|
|
"""
|
|
|
|
import time
|
2024-09-19 16:20:56 +02:00
|
|
|
from typing import List, Optional
|
2020-02-03 07:11:11 +01:00
|
|
|
|
|
|
|
import arrow
|
2022-07-01 11:14:53 +02:00
|
|
|
from sqlalchemy.sql.expression import or_, and_
|
2020-02-03 07:11:11 +01:00
|
|
|
|
2022-06-07 10:45:04 +02:00
|
|
|
from app import config
|
2021-10-12 14:36:47 +02:00
|
|
|
from app.db import Session
|
2020-09-10 20:14:55 +02:00
|
|
|
from app.email_utils import (
|
|
|
|
send_email,
|
|
|
|
render,
|
2020-04-02 23:26:17 +02:00
|
|
|
)
|
2024-08-23 09:11:47 +02:00
|
|
|
from app.events.event_dispatcher import PostgresDispatcher
|
2021-03-29 10:56:42 +02:00
|
|
|
from app.import_utils import handle_batch_import
|
2024-06-07 15:36:18 +02:00
|
|
|
from app.jobs.event_jobs import send_alias_creation_events_for_user
|
2022-06-07 10:45:04 +02:00
|
|
|
from app.jobs.export_user_data_job import ExportUserDataJob
|
2024-10-23 17:01:32 +02:00
|
|
|
from app.jobs.send_event_job import SendEventToWebhookJob
|
2020-02-03 07:11:11 +01:00
|
|
|
from app.log import LOG
|
2022-06-28 09:22:48 +02:00
|
|
|
from app.models import User, Job, BatchImport, Mailbox, CustomDomain, JobState
|
2024-10-16 16:57:59 +02:00
|
|
|
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
|
2021-03-29 10:56:42 +02:00
|
|
|
from server import create_light_app
|
2020-02-03 07:11:11 +01:00
|
|
|
|
|
|
|
|
2020-03-24 21:01:38 +01:00
|
|
|
def onboarding_send_from_alias(user):
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email, unsubscribe_link, via_email = user.get_communication_email()
|
|
|
|
if not comm_email:
|
2020-09-12 14:33:27 +02:00
|
|
|
return
|
|
|
|
|
2020-02-03 07:11:11 +01:00
|
|
|
send_email(
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email,
|
2020-12-06 11:25:41 +01:00
|
|
|
"SimpleLogin Tip: Send emails from your alias",
|
2022-12-13 16:59:14 +01:00
|
|
|
render(
|
|
|
|
"com/onboarding/send-from-alias.txt.j2",
|
|
|
|
user=user,
|
2022-12-19 09:23:53 +01:00
|
|
|
to_email=comm_email,
|
2022-12-13 16:59:14 +01:00
|
|
|
),
|
2022-12-19 09:23:53 +01:00
|
|
|
render("com/onboarding/send-from-alias.html", user=user, to_email=comm_email),
|
2020-10-22 10:44:05 +02:00
|
|
|
unsubscribe_link,
|
|
|
|
via_email,
|
2021-12-28 16:43:26 +01:00
|
|
|
retries=3,
|
2022-01-03 10:01:56 +01:00
|
|
|
ignore_smtp_error=True,
|
2020-02-03 07:11:11 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-03-24 21:01:38 +01:00
|
|
|
def onboarding_pgp(user):
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email, unsubscribe_link, via_email = user.get_communication_email()
|
|
|
|
if not comm_email:
|
2020-09-12 14:33:27 +02:00
|
|
|
return
|
|
|
|
|
2020-03-24 21:01:38 +01:00
|
|
|
send_email(
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email,
|
2020-12-06 11:25:41 +01:00
|
|
|
"SimpleLogin Tip: Secure your emails with PGP",
|
2022-12-19 09:23:53 +01:00
|
|
|
render("com/onboarding/pgp.txt", user=user, to_email=comm_email),
|
|
|
|
render("com/onboarding/pgp.html", user=user, to_email=comm_email),
|
2020-10-22 10:44:05 +02:00
|
|
|
unsubscribe_link,
|
|
|
|
via_email,
|
2021-12-28 16:43:26 +01:00
|
|
|
retries=3,
|
2022-01-03 10:01:56 +01:00
|
|
|
ignore_smtp_error=True,
|
2020-03-24 21:01:38 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-04-02 23:26:17 +02:00
|
|
|
def onboarding_browser_extension(user):
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email, unsubscribe_link, via_email = user.get_communication_email()
|
|
|
|
if not comm_email:
|
2020-09-12 14:33:27 +02:00
|
|
|
return
|
|
|
|
|
2020-04-02 23:26:17 +02:00
|
|
|
send_email(
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email,
|
2020-12-06 11:25:41 +01:00
|
|
|
"SimpleLogin Tip: Chrome/Firefox/Safari extensions and Android/iOS apps",
|
2022-12-13 16:59:14 +01:00
|
|
|
render(
|
2022-12-19 09:23:53 +01:00
|
|
|
"com/onboarding/browser-extension.txt",
|
|
|
|
user=user,
|
|
|
|
to_email=comm_email,
|
2022-12-13 16:59:14 +01:00
|
|
|
),
|
|
|
|
render(
|
|
|
|
"com/onboarding/browser-extension.html",
|
|
|
|
user=user,
|
2022-12-19 09:23:53 +01:00
|
|
|
to_email=comm_email,
|
2022-12-13 16:59:14 +01:00
|
|
|
),
|
2020-10-22 10:44:05 +02:00
|
|
|
unsubscribe_link,
|
|
|
|
via_email,
|
2021-12-28 16:43:26 +01:00
|
|
|
retries=3,
|
2022-01-03 10:01:56 +01:00
|
|
|
ignore_smtp_error=True,
|
2020-04-02 23:26:17 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-03-24 21:19:45 +01:00
|
|
|
def onboarding_mailbox(user):
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email, unsubscribe_link, via_email = user.get_communication_email()
|
|
|
|
if not comm_email:
|
2020-09-12 14:33:27 +02:00
|
|
|
return
|
|
|
|
|
2020-03-24 21:19:45 +01:00
|
|
|
send_email(
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email,
|
2020-12-06 11:25:41 +01:00
|
|
|
"SimpleLogin Tip: Multiple mailboxes",
|
2022-12-19 09:23:53 +01:00
|
|
|
render("com/onboarding/mailbox.txt", user=user, to_email=comm_email),
|
|
|
|
render("com/onboarding/mailbox.html", user=user, to_email=comm_email),
|
2022-12-19 11:45:04 +01:00
|
|
|
unsubscribe_link,
|
2020-10-22 10:44:05 +02:00
|
|
|
via_email,
|
2021-12-28 16:43:26 +01:00
|
|
|
retries=3,
|
2022-01-03 10:01:56 +01:00
|
|
|
ignore_smtp_error=True,
|
2020-03-24 21:19:45 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-06-20 11:36:16 +02:00
|
|
|
def welcome_proton(user):
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email, _, _ = user.get_communication_email()
|
|
|
|
if not comm_email:
|
2022-06-20 11:36:16 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
send_email(
|
2022-12-19 09:23:53 +01:00
|
|
|
comm_email,
|
2022-06-20 11:36:16 +02:00
|
|
|
"Welcome to SimpleLogin, an email masking service provided by Proton",
|
|
|
|
render(
|
|
|
|
"com/onboarding/welcome-proton-user.txt.jinja2",
|
|
|
|
user=user,
|
2022-12-19 09:23:53 +01:00
|
|
|
to_email=comm_email,
|
2022-12-13 16:59:14 +01:00
|
|
|
),
|
|
|
|
render(
|
|
|
|
"com/onboarding/welcome-proton-user.html",
|
|
|
|
user=user,
|
2022-12-19 09:23:53 +01:00
|
|
|
to_email=comm_email,
|
2022-06-20 11:36:16 +02:00
|
|
|
),
|
|
|
|
retries=3,
|
|
|
|
ignore_smtp_error=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-01-17 11:55:34 +01:00
|
|
|
def delete_mailbox_job(job: Job):
|
|
|
|
mailbox_id = job.payload.get("mailbox_id")
|
2024-10-16 16:57:59 +02:00
|
|
|
mailbox: Optional[Mailbox] = Mailbox.get(mailbox_id)
|
2023-01-17 11:55:34 +01:00
|
|
|
if not mailbox:
|
|
|
|
return
|
|
|
|
|
|
|
|
transfer_mailbox_id = job.payload.get("transfer_mailbox_id")
|
|
|
|
alias_transferred_to = None
|
|
|
|
if transfer_mailbox_id:
|
|
|
|
transfer_mailbox = Mailbox.get(transfer_mailbox_id)
|
|
|
|
if transfer_mailbox:
|
|
|
|
alias_transferred_to = transfer_mailbox.email
|
|
|
|
|
|
|
|
for alias in mailbox.aliases:
|
|
|
|
if alias.mailbox_id == mailbox.id:
|
|
|
|
alias.mailbox_id = transfer_mailbox.id
|
|
|
|
if transfer_mailbox in alias._mailboxes:
|
|
|
|
alias._mailboxes.remove(transfer_mailbox)
|
|
|
|
else:
|
|
|
|
alias._mailboxes.remove(mailbox)
|
|
|
|
if transfer_mailbox not in alias._mailboxes:
|
|
|
|
alias._mailboxes.append(transfer_mailbox)
|
|
|
|
Session.commit()
|
|
|
|
|
|
|
|
mailbox_email = mailbox.email
|
|
|
|
user = mailbox.user
|
2024-10-16 16:57:59 +02:00
|
|
|
|
|
|
|
emit_user_audit_log(
|
|
|
|
user=user,
|
|
|
|
action=UserAuditLogAction.DeleteMailbox,
|
|
|
|
message=f"Delete mailbox {mailbox.id} ({mailbox.email})",
|
|
|
|
)
|
2023-01-17 11:55:34 +01:00
|
|
|
Mailbox.delete(mailbox_id)
|
|
|
|
Session.commit()
|
|
|
|
LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email)
|
|
|
|
|
|
|
|
if alias_transferred_to:
|
|
|
|
send_email(
|
|
|
|
user.email,
|
|
|
|
f"Your mailbox {mailbox_email} has been deleted",
|
|
|
|
f"""Mailbox {mailbox_email} and its alias have been transferred to {alias_transferred_to}.
|
2023-01-25 13:16:29 +01:00
|
|
|
Regards,
|
|
|
|
SimpleLogin team.
|
|
|
|
""",
|
2023-01-17 11:55:34 +01:00
|
|
|
retries=3,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
send_email(
|
|
|
|
user.email,
|
|
|
|
f"Your mailbox {mailbox_email} has been deleted",
|
|
|
|
f"""Mailbox {mailbox_email} along with its aliases have been deleted successfully.
|
2023-01-25 13:16:29 +01:00
|
|
|
Regards,
|
|
|
|
SimpleLogin team.
|
|
|
|
""",
|
2023-01-17 11:55:34 +01:00
|
|
|
retries=3,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-06-28 09:22:48 +02:00
|
|
|
def process_job(job: Job):
|
|
|
|
if job.name == config.JOB_ONBOARDING_1:
|
|
|
|
user_id = job.payload.get("user_id")
|
|
|
|
user = User.get(user_id)
|
|
|
|
|
|
|
|
# user might delete their account in the meantime
|
|
|
|
# or disable the notification
|
|
|
|
if user and user.notification and user.activated:
|
|
|
|
LOG.d("send onboarding send-from-alias email to user %s", user)
|
|
|
|
onboarding_send_from_alias(user)
|
|
|
|
elif job.name == config.JOB_ONBOARDING_2:
|
|
|
|
user_id = job.payload.get("user_id")
|
|
|
|
user = User.get(user_id)
|
|
|
|
|
|
|
|
# user might delete their account in the meantime
|
|
|
|
# or disable the notification
|
|
|
|
if user and user.notification and user.activated:
|
|
|
|
LOG.d("send onboarding mailbox email to user %s", user)
|
|
|
|
onboarding_mailbox(user)
|
|
|
|
elif job.name == config.JOB_ONBOARDING_4:
|
|
|
|
user_id = job.payload.get("user_id")
|
2024-06-10 13:58:04 +02:00
|
|
|
user: User = User.get(user_id)
|
2022-06-28 09:22:48 +02:00
|
|
|
|
|
|
|
# user might delete their account in the meantime
|
|
|
|
# or disable the notification
|
|
|
|
if user and user.notification and user.activated:
|
2024-06-10 13:58:04 +02:00
|
|
|
# if user only has 1 mailbox which is Proton then do not send PGP onboarding email
|
|
|
|
mailboxes = user.mailboxes()
|
|
|
|
if len(mailboxes) == 1 and mailboxes[0].is_proton():
|
|
|
|
LOG.d("Do not send onboarding PGP email to Proton mailbox")
|
|
|
|
else:
|
|
|
|
LOG.d("send onboarding pgp email to user %s", user)
|
|
|
|
onboarding_pgp(user)
|
2022-06-28 09:22:48 +02:00
|
|
|
|
|
|
|
elif job.name == config.JOB_BATCH_IMPORT:
|
|
|
|
batch_import_id = job.payload.get("batch_import_id")
|
|
|
|
batch_import = BatchImport.get(batch_import_id)
|
|
|
|
handle_batch_import(batch_import)
|
|
|
|
elif job.name == config.JOB_DELETE_ACCOUNT:
|
|
|
|
user_id = job.payload.get("user_id")
|
|
|
|
user = User.get(user_id)
|
|
|
|
|
|
|
|
if not user:
|
|
|
|
LOG.i("No user found for %s", user_id)
|
|
|
|
return
|
|
|
|
|
|
|
|
user_email = user.email
|
|
|
|
LOG.w("Delete user %s", user)
|
|
|
|
send_email(
|
|
|
|
user_email,
|
|
|
|
"Your SimpleLogin account has been deleted",
|
2024-07-03 14:59:16 +02:00
|
|
|
render("transactional/account-delete.txt", user=user),
|
|
|
|
render("transactional/account-delete.html", user=user),
|
2022-06-28 09:22:48 +02:00
|
|
|
retries=3,
|
|
|
|
)
|
2024-07-03 14:59:16 +02:00
|
|
|
User.delete(user.id)
|
|
|
|
Session.commit()
|
2022-06-28 09:22:48 +02:00
|
|
|
elif job.name == config.JOB_DELETE_MAILBOX:
|
2023-01-17 11:55:34 +01:00
|
|
|
delete_mailbox_job(job)
|
2022-06-28 09:22:48 +02:00
|
|
|
|
|
|
|
elif job.name == config.JOB_DELETE_DOMAIN:
|
|
|
|
custom_domain_id = job.payload.get("custom_domain_id")
|
2024-09-19 16:20:56 +02:00
|
|
|
custom_domain: Optional[CustomDomain] = CustomDomain.get(custom_domain_id)
|
2022-06-28 09:22:48 +02:00
|
|
|
if not custom_domain:
|
|
|
|
return
|
|
|
|
|
2024-10-16 16:57:59 +02:00
|
|
|
is_subdomain = custom_domain.is_sl_subdomain
|
2022-06-28 09:22:48 +02:00
|
|
|
domain_name = custom_domain.domain
|
|
|
|
user = custom_domain.user
|
|
|
|
|
2024-10-02 09:45:54 +02:00
|
|
|
custom_domain_partner_id = custom_domain.partner_id
|
2022-06-28 09:22:48 +02:00
|
|
|
CustomDomain.delete(custom_domain.id)
|
|
|
|
Session.commit()
|
|
|
|
|
2024-10-16 16:57:59 +02:00
|
|
|
if is_subdomain:
|
|
|
|
message = f"Delete subdomain {custom_domain_id} ({domain_name})"
|
|
|
|
else:
|
|
|
|
message = f"Delete custom domain {custom_domain_id} ({domain_name})"
|
|
|
|
emit_user_audit_log(
|
|
|
|
user=user,
|
|
|
|
action=UserAuditLogAction.DeleteCustomDomain,
|
|
|
|
message=message,
|
|
|
|
)
|
|
|
|
|
2022-06-28 09:22:48 +02:00
|
|
|
LOG.d("Domain %s deleted", domain_name)
|
|
|
|
|
2024-10-02 09:45:54 +02:00
|
|
|
if custom_domain_partner_id is None:
|
2024-09-19 16:20:56 +02:00
|
|
|
send_email(
|
|
|
|
user.email,
|
|
|
|
f"Your domain {domain_name} has been deleted",
|
|
|
|
f"""Domain {domain_name} along with its aliases are deleted successfully.
|
|
|
|
|
|
|
|
Regards,
|
|
|
|
SimpleLogin team.
|
|
|
|
""",
|
|
|
|
retries=3,
|
|
|
|
)
|
2022-06-28 09:22:48 +02:00
|
|
|
elif job.name == config.JOB_SEND_USER_REPORT:
|
|
|
|
export_job = ExportUserDataJob.create_from_job(job)
|
|
|
|
if export_job:
|
|
|
|
export_job.run()
|
|
|
|
elif job.name == config.JOB_SEND_PROTON_WELCOME_1:
|
|
|
|
user_id = job.payload.get("user_id")
|
|
|
|
user = User.get(user_id)
|
|
|
|
if user and user.activated:
|
2024-06-07 15:36:18 +02:00
|
|
|
LOG.d("Send proton welcome email to user %s", user)
|
2022-06-28 09:22:48 +02:00
|
|
|
welcome_proton(user)
|
2024-06-07 15:36:18 +02:00
|
|
|
elif job.name == config.JOB_SEND_ALIAS_CREATION_EVENTS:
|
|
|
|
user_id = job.payload.get("user_id")
|
|
|
|
user = User.get(user_id)
|
|
|
|
if user and user.activated:
|
|
|
|
LOG.d(f"Sending alias creation events for {user}")
|
2024-08-23 09:11:47 +02:00
|
|
|
send_alias_creation_events_for_user(
|
|
|
|
user, dispatcher=PostgresDispatcher.get()
|
|
|
|
)
|
2024-10-23 17:01:32 +02:00
|
|
|
elif job.name == config.JOB_SEND_EVENT_TO_WEBHOOK:
|
|
|
|
send_job = SendEventToWebhookJob.create_from_job(job)
|
|
|
|
if send_job:
|
|
|
|
send_job.run()
|
2022-06-28 09:22:48 +02:00
|
|
|
else:
|
|
|
|
LOG.e("Unknown job name %s", job.name)
|
|
|
|
|
|
|
|
|
2022-07-01 11:14:53 +02:00
|
|
|
def get_jobs_to_run() -> List[Job]:
|
|
|
|
# Get jobs that match all conditions:
|
|
|
|
# - Job.state == ready OR (Job.state == taken AND Job.taken_at < now - 30 mins AND Job.attempts < 5)
|
|
|
|
# - Job.run_at is Null OR Job.run_at < now + 10 mins
|
|
|
|
taken_at_earliest = arrow.now().shift(minutes=-config.JOB_TAKEN_RETRY_WAIT_MINS)
|
|
|
|
run_at_earliest = arrow.now().shift(minutes=+10)
|
|
|
|
query = Job.filter(
|
|
|
|
and_(
|
|
|
|
or_(
|
|
|
|
Job.state == JobState.ready.value,
|
|
|
|
and_(
|
|
|
|
Job.state == JobState.taken.value,
|
|
|
|
Job.taken_at < taken_at_earliest,
|
|
|
|
Job.attempts < config.JOB_MAX_ATTEMPTS,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
or_(Job.run_at.is_(None), and_(Job.run_at <= run_at_earliest)),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return query.all()
|
|
|
|
|
|
|
|
|
2020-02-03 07:11:11 +01:00
|
|
|
if __name__ == "__main__":
|
|
|
|
while True:
|
2021-10-26 10:52:28 +02:00
|
|
|
# wrap in an app context to benefit from app setup like database cleanup, sentry integration, etc
|
|
|
|
with create_light_app().app_context():
|
2022-07-01 11:14:53 +02:00
|
|
|
for job in get_jobs_to_run():
|
2021-10-26 10:52:28 +02:00
|
|
|
LOG.d("Take job %s", job)
|
|
|
|
|
|
|
|
# mark the job as taken, whether it will be executed successfully or not
|
|
|
|
job.taken = True
|
2022-06-28 09:22:48 +02:00
|
|
|
job.taken_at = arrow.now()
|
|
|
|
job.state = JobState.taken.value
|
|
|
|
job.attempts += 1
|
2021-10-13 11:43:44 +02:00
|
|
|
Session.commit()
|
2022-06-28 09:22:48 +02:00
|
|
|
process_job(job)
|
2021-10-13 11:43:44 +02:00
|
|
|
|
2022-06-28 09:22:48 +02:00
|
|
|
job.state = JobState.done.value
|
|
|
|
Session.commit()
|
2021-10-26 10:52:28 +02:00
|
|
|
|
|
|
|
time.sleep(10)
|