Compare commits

...

4 Commits

Author SHA1 Message Date
9fd2fa9a78 4.61.1
All checks were successful
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m41s
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 4m6s
Build-Release-Image / Merge-Images (push) Successful in 18s
Build-Release-Image / Create-Release (push) Successful in 11s
Build-Release-Image / Notify (push) Successful in 3s
2024-11-30 12:00:10 +00:00
3c77f8af4b 4.61.0
All checks were successful
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 4m9s
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 4m14s
Build-Release-Image / Merge-Images (push) Successful in 47s
Build-Release-Image / Create-Release (push) Successful in 16s
Build-Release-Image / Notify (push) Successful in 3s
2024-11-29 12:00:12 +00:00
545eeda79b 4.59.5
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m2s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m43s
Build-Release-Image / Merge-Images (push) Successful in 49s
Build-Release-Image / Create-Release (push) Successful in 21s
Build-Release-Image / Notify (push) Successful in 8s
2024-11-18 12:00:06 +00:00
01dba12ed0 4.59.3
All checks were successful
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 3m43s
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m57s
Build-Release-Image / Merge-Images (push) Successful in 53s
Build-Release-Image / Create-Release (push) Successful in 8s
Build-Release-Image / Notify (push) Successful in 3s
2024-11-16 12:00:07 +00:00
20 changed files with 576 additions and 162 deletions

View File

@ -4,6 +4,7 @@ from enum import Enum
from typing import Optional
import arrow
import sqlalchemy.exc
from arrow import Arrow
from newrelic import agent
from psycopg2.errors import UniqueViolation
@ -35,6 +36,7 @@ from app.utils import random_string
class SLPlanType(Enum):
Free = 1
Premium = 2
PremiumLifetime = 3
@dataclass
@ -75,6 +77,7 @@ def send_user_plan_changed_event(partner_user: PartnerUser) -> Optional[int]:
def set_plan_for_partner_user(partner_user: PartnerUser, plan: SLPlan):
sub = PartnerSubscription.get_by(partner_user_id=partner_user.id)
is_lifetime = plan.type == SLPlanType.PremiumLifetime
if plan.type == SLPlanType.Free:
if sub is not None:
LOG.i(
@ -83,25 +86,30 @@ def set_plan_for_partner_user(partner_user: PartnerUser, plan: SLPlan):
PartnerSubscription.delete(sub.id)
agent.record_custom_event("PlanChange", {"plan": "free"})
else:
end_time = plan.expiration
if plan.type == SLPlanType.PremiumLifetime:
end_time = None
if sub is None:
LOG.i(
f"Creating partner_subscription [user_id={partner_user.user_id}] [partner_id={partner_user.partner_id}]"
f"Creating partner_subscription [user_id={partner_user.user_id}] [partner_id={partner_user.partner_id}] with {end_time} / {is_lifetime}"
)
create_partner_subscription(
partner_user=partner_user,
expiration=plan.expiration,
expiration=end_time,
lifetime=is_lifetime,
msg="Upgraded via partner. User did not have a previous partner subscription",
)
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}]"
)
if sub.end_at != plan.expiration or sub.lifetime != is_lifetime:
agent.record_custom_event(
"PlanChange", {"plan": "premium", "type": "extension"}
)
sub.end_at = plan.expiration
sub.end_at = plan.expiration if not is_lifetime else None
sub.lifetime = is_lifetime
LOG.i(
f"Updating partner_subscription [user_id={partner_user.user_id}] [partner_id={partner_user.partner_id}] to {sub.end_at} / {sub.lifetime} "
)
emit_user_audit_log(
user=partner_user.user,
action=UserAuditLogAction.SubscriptionExtended,
@ -185,7 +193,8 @@ class NewUserStrategy(ClientMergeStrategy):
user=new_user,
strategy=self.__class__.__name__,
)
except UniqueViolation:
except (UniqueViolation, sqlalchemy.exc.IntegrityError) as e:
LOG.debug(f"Got the duplicate user error: {e}")
return self.create_missing_link(canonical_email)
def create_missing_link(self, canonical_email: str):

View File

@ -174,7 +174,12 @@ def setting():
flash("Your preference has been updated", "success")
return redirect(url_for("dashboard.setting"))
elif request.form.get("form-name") == "random-alias-suffix":
scheme = int(request.form.get("random-alias-suffix-generator"))
try:
scheme = int(request.form.get("random-alias-suffix-generator"))
except ValueError:
flash("Invalid value", "error")
return redirect(url_for("dashboard.setting"))
if AliasSuffixEnum.has_value(scheme):
current_user.random_alias_suffix = scheme
Session.commit()

View File

@ -1345,17 +1345,16 @@ def get_queue_id(msg: Message) -> Optional[str]:
received_header = str(msg[headers.RECEIVED])
if not received_header:
return
return None
# received_header looks like 'from mail-wr1-x434.google.com (mail-wr1-x434.google.com [IPv6:2a00:1450:4864:20::434])\r\n\t(using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits))\r\n\t(No client certificate requested)\r\n\tby mx1.simplelogin.co (Postfix) with ESMTPS id 4FxQmw1DXdz2vK2\r\n\tfor <jglfdjgld@alias.com>; Fri, 4 Jun 2021 14:55:43 +0000 (UTC)'
search_result = re.search("with ESMTPS id [0-9a-zA-Z]{1,}", received_header)
if not search_result:
return
# the "with ESMTPS id 4FxQmw1DXdz2vK2" part
with_esmtps = received_header[search_result.start() : search_result.end()]
return with_esmtps[len("with ESMTPS id ") :]
search_result = re.search(r"with E?SMTP[AS]? id ([0-9a-zA-Z]{1,})", received_header)
if search_result:
return search_result.group(1)
search_result = re.search("\(Postfix\)\r\n\tid ([a-zA-Z0-9]{1,});", received_header)
if search_result:
return search_result.group(1)
return None
def should_ignore_bounce(mail_from: str) -> bool:

View File

@ -12,11 +12,13 @@ from app.email_utils import (
email_can_be_used_as_mailbox,
send_email,
render,
get_email_domain_part,
)
from app.email_validation import is_valid_email
from app.log import LOG
from app.models import User, Mailbox, Job, MailboxActivation
from app.models import User, Mailbox, Job, MailboxActivation, Alias
from app.user_audit_log_utils import emit_user_audit_log, UserAuditLogAction
from app.utils import canonicalize_email, sanitize_email
@dataclasses.dataclass
@ -52,6 +54,7 @@ def create_mailbox(
use_digit_codes: bool = False,
send_link: bool = True,
) -> CreateMailboxOutput:
email = sanitize_email(email)
if not user.is_premium():
LOG.i(
f"User {user} has tried to create mailbox with {email} but is not premium"
@ -104,7 +107,10 @@ def create_mailbox(
def delete_mailbox(
user: User, mailbox_id: int, transfer_mailbox_id: Optional[int]
user: User,
mailbox_id: int,
transfer_mailbox_id: Optional[int],
send_mail: bool = True,
) -> Mailbox:
mailbox = Mailbox.get(mailbox_id)
@ -150,6 +156,7 @@ def delete_mailbox(
"transfer_mailbox_id": transfer_mailbox_id
if transfer_mailbox_id and transfer_mailbox_id > 0
else None,
"send_mail": send_mail,
},
run_at=arrow.now(),
commit=True,
@ -328,3 +335,56 @@ def perform_mailbox_email_change(mailbox_id: int) -> MailboxEmailChangeResult:
message="Invalid link",
message_category="error",
)
def __get_alias_mailbox_from_email(
email_address: str, alias: Alias
) -> Optional[Mailbox]:
for mailbox in alias.mailboxes:
if mailbox.email == email_address:
return mailbox
for authorized_address in mailbox.authorized_addresses:
if authorized_address.email == email_address:
LOG.d(
"Found an authorized address for %s %s %s",
alias,
mailbox,
authorized_address,
)
return mailbox
return None
def __get_alias_mailbox_from_email_or_canonical_email(
email_address: str, alias: Alias
) -> Optional[Mailbox]:
# We need to first check for the uncanonicalized version because we still have users in the db with the
# email non canonicalized. So if it matches the already existing one use that, otherwise check the canonical one
mbox = __get_alias_mailbox_from_email(email_address, alias)
if mbox is not None:
return mbox
canonical_email = canonicalize_email(email_address)
if canonical_email != email_address:
return __get_alias_mailbox_from_email(canonical_email, alias)
return None
def get_mailbox_for_reply_phase(
envelope_mail_from: str, header_mail_from: str, alias
) -> Optional[Mailbox]:
"""return the corresponding mailbox given the mail_from and alias
Usually the mail_from=mailbox.email but it can also be one of the authorized address
"""
mbox = __get_alias_mailbox_from_email_or_canonical_email(envelope_mail_from, alias)
if mbox is not None:
return mbox
if not header_mail_from:
return None
envelope_from_domain = get_email_domain_part(envelope_mail_from)
header_from_domain = get_email_domain_part(header_mail_from)
if envelope_from_domain != header_from_domain:
return None
# For services that use VERP sending (envelope from has encoded data to account for bounces)
# if the domain is the same in the header from as the envelope from we can use the header from
return __get_alias_mailbox_from_email_or_canonical_email(header_mail_from, alias)

View File

@ -158,6 +158,8 @@ class File(Base, ModelMixin):
path = sa.Column(sa.String(128), unique=True, nullable=False)
user_id = sa.Column(sa.ForeignKey("users.id", ondelete="cascade"), nullable=True)
__table_args__ = (sa.Index("ix_file_user_id", "user_id"),)
def get_url(self, expires_in=3600):
return s3.get_url(self.path, expires_in)
@ -319,6 +321,8 @@ class HibpNotifiedAlias(Base, ModelMixin):
notified_at = sa.Column(ArrowType, default=arrow.utcnow, nullable=False)
__table_args__ = (sa.Index("ix_hibp_notified_alias_user_id", "user_id"),)
class Fido(Base, ModelMixin):
__tablename__ = "fido"
@ -333,6 +337,8 @@ class Fido(Base, ModelMixin):
name = sa.Column(sa.String(128), nullable=False, unique=False)
user_id = sa.Column(sa.ForeignKey("users.id", ondelete="cascade"), nullable=True)
__table_args__ = (sa.Index("ix_fido_user_id", "user_id"),)
class User(Base, ModelMixin, UserMixin, PasswordOracle):
__tablename__ = "users"
@ -566,6 +572,10 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
),
sa.Index("ix_users_delete_on", delete_on),
sa.Index("ix_users_default_mailbox_id", default_mailbox_id),
sa.Index(
"ix_users_default_alias_custom_domain_id", default_alias_custom_domain_id
),
sa.Index("ix_users_profile_picture_id", profile_picture_id),
)
@property
@ -1222,6 +1232,8 @@ class ActivationCode(Base, ModelMixin):
expired = sa.Column(ArrowType, nullable=False, default=_expiration_1h)
__table_args__ = (sa.Index("ix_activation_code_user_id", "user_id"),)
def is_expired(self):
return self.expired < arrow.now()
@ -1238,6 +1250,8 @@ class ResetPasswordCode(Base, ModelMixin):
expired = sa.Column(ArrowType, nullable=False, default=_expiration_1h)
__table_args__ = (sa.Index("ix_reset_password_code_user_id", "user_id"),)
def is_expired(self):
return self.expired < arrow.now()
@ -1280,6 +1294,8 @@ class MfaBrowser(Base, ModelMixin):
user = orm.relationship(User)
__table_args__ = (sa.Index("ix_mfa_browser_user_id", "user_id"),)
@classmethod
def create_new(cls, user, token_length=64) -> "MfaBrowser":
found = False
@ -1338,6 +1354,12 @@ class Client(Base, ModelMixin):
user = orm.relationship(User)
referral = orm.relationship("Referral")
__table_args__ = (
sa.Index("ix_client_user_id", "user_id"),
sa.Index("ix_client_icon_id", "icon_id"),
sa.Index("ix_client_referral_id", "referral_id"),
)
def nb_user(self):
return ClientUser.filter_by(client_id=self.id).count()
@ -1386,6 +1408,8 @@ class RedirectUri(Base, ModelMixin):
client = orm.relationship(Client, backref="redirect_uris")
__table_args__ = (sa.Index("ix_redirect_uri_client_id", "client_id"),)
class AuthorizationCode(Base, ModelMixin):
__tablename__ = "authorization_code"
@ -1407,6 +1431,11 @@ class AuthorizationCode(Base, ModelMixin):
expired = sa.Column(ArrowType, nullable=False, default=_expiration_5m)
__table_args__ = (
sa.Index("ix_authorization_code_client_id", "client_id"),
sa.Index("ix_authorization_code_user_id", "user_id"),
)
def is_expired(self):
return self.expired < arrow.now()
@ -1429,6 +1458,11 @@ class OauthToken(Base, ModelMixin):
expired = sa.Column(ArrowType, nullable=False, default=_expiration_1h)
__table_args__ = (
sa.Index("ix_oauth_token_user_id", "user_id"),
sa.Index("ix_oauth_token_client_id", "client_id"),
)
def is_expired(self):
return self.expired < arrow.now()
@ -1582,6 +1616,7 @@ class Alias(Base, ModelMixin):
postgresql_ops={"note": "gin_trgm_ops"},
postgresql_using="gin",
),
Index("ix_alias_original_owner_id", "original_owner_id"),
)
user = orm.relationship(User, foreign_keys=[user_id])
@ -2079,6 +2114,7 @@ class EmailLog(Base, ModelMixin):
Index("ix_email_log_created_at", "created_at"),
Index("ix_email_log_mailbox_id", "mailbox_id"),
Index("ix_email_log_bounced_mailbox_id", "bounced_mailbox_id"),
Index("ix_email_log_refused_email_id", "refused_email_id"),
)
user_id = sa.Column(
@ -2355,6 +2391,7 @@ class AliasUsedOn(Base, ModelMixin):
__table_args__ = (
sa.UniqueConstraint("alias_id", "hostname", name="uq_alias_used"),
sa.Index("ix_alias_used_on_user_id", "user_id"),
)
alias_id = sa.Column(
@ -2381,6 +2418,11 @@ class ApiKey(Base, ModelMixin):
user = orm.relationship(User)
__table_args__ = (
sa.Index("ix_api_key_code", "code"),
sa.Index("ix_api_key_user_id", "user_id"),
)
@classmethod
def create(cls, user_id, name=None, **kwargs):
code = random_string(60)
@ -2539,6 +2581,7 @@ class AutoCreateRule(Base, ModelMixin):
sa.UniqueConstraint(
"custom_domain_id", "order", name="uq_auto_create_rule_order"
),
sa.Index("ix_auto_create_rule_custom_domain_id", "custom_domain_id"),
)
custom_domain_id = sa.Column(
@ -2582,6 +2625,7 @@ class DomainDeletedAlias(Base, ModelMixin):
__table_args__ = (
sa.UniqueConstraint("domain_id", "email", name="uq_domain_trash"),
sa.Index("ix_domain_deleted_alias_user_id", "user_id"),
)
email = sa.Column(sa.String(256), nullable=False)
@ -2642,6 +2686,8 @@ class Coupon(Base, ModelMixin):
# a coupon can have an expiration
expires_date = sa.Column(ArrowType, nullable=True)
__table_args__ = (sa.Index("ix_coupon_used_by_user_id", "used_by_user_id"),)
class Directory(Base, ModelMixin):
__tablename__ = "directory"
@ -2656,6 +2702,8 @@ class Directory(Base, ModelMixin):
"Mailbox", secondary="directory_mailbox", lazy="joined"
)
__table_args__ = (sa.Index("ix_directory_user_id", "user_id"),)
@property
def mailboxes(self):
if self._mailboxes:
@ -2897,6 +2945,8 @@ class RefusedEmail(Base, ModelMixin):
# toggle this when email content (stored at full_report_path & path are deleted)
deleted = sa.Column(sa.Boolean, nullable=False, default=False, server_default="0")
__table_args__ = (sa.Index("ix_refused_email_user_id", "user_id"),)
def get_url(self, expires_in=3600):
if self.path:
return s3.get_url(self.path, expires_in)
@ -2919,6 +2969,8 @@ class Referral(Base, ModelMixin):
user = orm.relationship(User, foreign_keys=[user_id], backref="referrals")
__table_args__ = (sa.Index("ix_referral_user_id", "user_id"),)
@property
def nb_user(self) -> int:
return User.filter_by(referral_id=self.id, activated=True).count()
@ -2958,6 +3010,8 @@ class SentAlert(Base, ModelMixin):
to_email = sa.Column(sa.String(256), nullable=False)
alert_type = sa.Column(sa.String(256), nullable=False)
__table_args__ = (sa.Index("ix_sent_alert_user_id", "user_id"),)
class AliasMailbox(Base, ModelMixin):
__tablename__ = "alias_mailbox"
@ -3203,6 +3257,11 @@ class BatchImport(Base, ModelMixin):
file = orm.relationship(File)
user = orm.relationship(User)
__table_args__ = (
sa.Index("ix_batch_import_file_id", "file_id"),
sa.Index("ix_batch_import_user_id", "user_id"),
)
def nb_alias(self):
return Alias.filter_by(batch_import_id=self.id).count()
@ -3223,6 +3282,7 @@ class AuthorizedAddress(Base, ModelMixin):
__table_args__ = (
sa.UniqueConstraint("mailbox_id", "email", name="uq_authorize_address"),
sa.Index("ix_authorized_address_user_id", "user_id"),
)
mailbox = orm.relationship(Mailbox, backref="authorized_addresses")
@ -3364,6 +3424,8 @@ class Payout(Base, ModelMixin):
user = orm.relationship(User)
__table_args__ = (sa.Index("ix_payout_user_id", "user_id"),)
class IgnoredEmail(Base, ModelMixin):
"""If an email has mail_from and rcpt_to present in this table, discard it by returning 250 status."""
@ -3465,6 +3527,8 @@ class PhoneReservation(Base, ModelMixin):
start = sa.Column(ArrowType, nullable=False)
end = sa.Column(ArrowType, nullable=False)
__table_args__ = (sa.Index("ix_phone_reservation_user_id", "user_id"),)
class PhoneMessage(Base, ModelMixin):
__tablename__ = "phone_message"
@ -3639,6 +3703,11 @@ class ProviderComplaint(Base, ModelMixin):
user = orm.relationship(User, foreign_keys=[user_id])
refused_email = orm.relationship(RefusedEmail, foreign_keys=[refused_email_id])
__table_args__ = (
sa.Index("ix_provider_complaint_user_id", "user_id"),
sa.Index("ix_provider_complaint_refused_email_id", "refused_email_id"),
)
class PartnerApiToken(Base, ModelMixin):
__tablename__ = "partner_api_token"
@ -3709,7 +3778,8 @@ class PartnerSubscription(Base, ModelMixin):
)
# when the partner subscription ends
end_at = sa.Column(ArrowType, nullable=False, index=True)
end_at = sa.Column(ArrowType, nullable=True, index=True)
lifetime = sa.Column(sa.Boolean, default=False, nullable=False, server_default="0")
partner_user = orm.relationship(PartnerUser)
@ -3731,7 +3801,9 @@ class PartnerSubscription(Base, ModelMixin):
return None
def is_active(self):
return self.end_at > arrow.now().shift(days=-_PARTNER_SUBSCRIPTION_GRACE_DAYS)
return self.lifetime or self.end_at > arrow.now().shift(
days=-_PARTNER_SUBSCRIPTION_GRACE_DAYS
)
# endregion
@ -3762,6 +3834,8 @@ class NewsletterUser(Base, ModelMixin):
user = orm.relationship(User)
newsletter = orm.relationship(Newsletter)
__table_args__ = (sa.Index("ix_newsletter_user_user_id", "user_id"),)
class ApiToCookieToken(Base, ModelMixin):
__tablename__ = "api_cookie_token"
@ -3772,6 +3846,11 @@ class ApiToCookieToken(Base, ModelMixin):
user = orm.relationship(User)
api_key = orm.relationship(ApiKey)
__table_args__ = (
sa.Index("ix_api_to_cookie_token_api_key_id", "api_key_id"),
sa.Index("ix_api_to_cookie_token_user_id", "user_id"),
)
@classmethod
def create(cls, **kwargs):
code = secrets.token_urlsafe(32)

View File

@ -33,12 +33,14 @@ def create_partner_user(
def create_partner_subscription(
partner_user: PartnerUser,
expiration: Optional[Arrow],
expiration: Optional[Arrow] = None,
lifetime: bool = False,
msg: Optional[str] = None,
) -> PartnerSubscription:
instance = PartnerSubscription.create(
partner_user_id=partner_user.id,
end_at=expiration,
lifetime=lifetime,
)
message = "User upgraded through partner subscription"

View File

@ -16,6 +16,7 @@ PROTON_ERROR_CODE_HV_NEEDED = 9001
PLAN_FREE = 1
PLAN_PREMIUM = 2
PLAN_PREMIUM_LIFETIME = 3
@dataclass
@ -112,10 +113,13 @@ class HttpProtonClient(ProtonClient):
if plan_value == PLAN_FREE:
plan = SLPlan(type=SLPlanType.Free, expiration=None)
elif plan_value == PLAN_PREMIUM:
expiration = info.get("PlanExpiration", "1")
plan = SLPlan(
type=SLPlanType.Premium,
expiration=Arrow.fromtimestamp(info["PlanExpiration"], tzinfo="utc"),
expiration=Arrow.fromtimestamp(expiration, tzinfo="utc"),
)
elif plan_value == PLAN_PREMIUM_LIFETIME:
plan = SLPlan(SLPlanType.PremiumLifetime, expiration=None)
else:
raise Exception(f"Invalid value for plan: {plan_value}")

View File

@ -14,8 +14,9 @@ jobs:
- name: SimpleLogin Custom Domain check
command: python /code/cron.py -j check_custom_domain
shell: /bin/bash
schedule: "15 2 * * *"
schedule: "15 */4 * * *"
captureStderr: true
concurrencyPolicy: Forbid
onFailure:
retry:
maximumRetries: 10
@ -26,7 +27,7 @@ jobs:
- name: SimpleLogin HIBP check
command: python /code/cron.py -j check_hibp
shell: /bin/bash
schedule: "16 */4 * * *"
schedule: "*/5 * * * *"
captureStderr: true
concurrencyPolicy: Forbid
onFailure:

View File

@ -149,6 +149,7 @@ from app.handler.unsubscribe_generator import UnsubscribeGenerator
from app.handler.unsubscribe_handler import UnsubscribeHandler
from app.log import LOG, set_message_id
from app.mail_sender import sl_sendmail
from app.mailbox_utils import get_mailbox_for_reply_phase
from app.message_utils import message_to_bytes
from app.models import (
Alias,
@ -172,7 +173,7 @@ from app.pgp_utils import (
sign_data,
load_public_key_and_check,
)
from app.utils import sanitize_email, canonicalize_email
from app.utils import sanitize_email
from init_app import load_pgp_public_keys
from server import create_light_app
@ -1008,7 +1009,6 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
return False, status.E503
user = alias.user
mail_from = envelope.mail_from
if not user.can_send_or_receive():
LOG.i(f"User {user} cannot send emails")
@ -1022,13 +1022,15 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
return False, dmarc_delivery_status
# Anti-spoofing
mailbox = get_mailbox_from_mail_from(mail_from, alias)
mailbox = get_mailbox_for_reply_phase(
envelope.mail_from, get_header_unicode(msg[headers.FROM]), alias
)
if not mailbox:
if alias.disable_email_spoofing_check:
# ignore this error, use default alias mailbox
LOG.w(
"ignore unknown sender to reverse-alias %s: %s -> %s",
mail_from,
envelope.mail_from,
alias,
contact,
)
@ -1367,32 +1369,6 @@ def replace_original_message_id(alias: Alias, email_log: EmailLog, msg: Message)
msg[headers.REFERENCES] = " ".join(new_message_ids)
def get_mailbox_from_mail_from(mail_from: str, alias) -> Optional[Mailbox]:
"""return the corresponding mailbox given the mail_from and alias
Usually the mail_from=mailbox.email but it can also be one of the authorized address
"""
def __check(email_address: str, alias: Alias) -> Optional[Mailbox]:
for mailbox in alias.mailboxes:
if mailbox.email == email_address:
return mailbox
for authorized_address in mailbox.authorized_addresses:
if authorized_address.email == email_address:
LOG.d(
"Found an authorized address for %s %s %s",
alias,
mailbox,
authorized_address,
)
return mailbox
return None
# We need to first check for the uncanonicalized version because we still have users in the db with the
# email non canonicalized. So if it matches the already existing one use that, otherwise check the canonical one
return __check(mail_from, alias) or __check(canonicalize_email(mail_from), alias)
def handle_unknown_mailbox(
envelope, msg, reply_email: str, user: User, alias: Alias, contact: Contact
):

View File

@ -164,6 +164,8 @@ def delete_mailbox_job(job: Job):
Session.commit()
LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email)
if not job.payload.get("send_mail", True):
return
if alias_transferred_to:
send_email(
user.email,

View File

@ -1,6 +1,4 @@
abacus
abdomen
abdominal
abide
abiding
ability
@ -1031,7 +1029,6 @@ chosen
chowder
chowtime
chrome
chubby
chuck
chug
chummy
@ -2041,8 +2038,6 @@ dwindling
dynamic
dynamite
dynasty
dyslexia
dyslexic
each
eagle
earache
@ -2081,7 +2076,6 @@ eatery
eating
eats
ebay
ebony
ebook
ecard
eccentric
@ -2375,8 +2369,6 @@ exclude
excluding
exclusion
exclusive
excretion
excretory
excursion
excusable
excusably
@ -2396,8 +2388,6 @@ existing
exit
exodus
exonerate
exorcism
exorcist
expand
expanse
expansion
@ -2483,7 +2473,6 @@ fanning
fantasize
fantastic
fantasy
fascism
fastball
faster
fasting
@ -3028,7 +3017,6 @@ guiding
guileless
guise
gulf
gullible
gully
gulp
gumball
@ -3040,10 +3028,6 @@ gurgle
gurgling
guru
gush
gusto
gusty
gutless
guts
gutter
guy
guzzler
@ -3242,8 +3226,6 @@ humble
humbling
humbly
humid
humiliate
humility
humming
hummus
humongous
@ -3271,7 +3253,6 @@ hurray
hurricane
hurried
hurry
hurt
husband
hush
husked
@ -3292,8 +3273,6 @@ hypnotic
hypnotism
hypnotist
hypnotize
hypocrisy
hypocrite
ibuprofen
ice
iciness
@ -3323,7 +3302,6 @@ image
imaginary
imagines
imaging
imbecile
imitate
imitation
immerse
@ -3746,7 +3724,6 @@ machine
machinist
magazine
magenta
maggot
magical
magician
magma
@ -3968,8 +3945,6 @@ multitude
mumble
mumbling
mumbo
mummified
mummify
mumps
munchkin
mundane
@ -4022,8 +3997,6 @@ napped
napping
nappy
narrow
nastily
nastiness
national
native
nativity
@ -4446,7 +4419,6 @@ pasta
pasted
pastel
pastime
pastor
pastrami
pasture
pasty
@ -4458,7 +4430,6 @@ path
patience
patient
patio
patriarch
patriot
patrol
patronage
@ -4549,7 +4520,6 @@ pettiness
petty
petunia
phantom
phobia
phoenix
phonebook
phoney
@ -4608,7 +4578,6 @@ plot
plow
ploy
pluck
plug
plunder
plunging
plural
@ -4875,7 +4844,6 @@ pupil
puppet
puppy
purchase
pureblood
purebred
purely
pureness
@ -5047,7 +5015,6 @@ recharger
recipient
recital
recite
reckless
reclaim
recliner
reclining
@ -5440,7 +5407,6 @@ rubdown
ruby
ruckus
rudder
rug
ruined
rule
rumble
@ -5448,7 +5414,6 @@ rumbling
rummage
rumor
runaround
rundown
runner
running
runny
@ -5518,7 +5483,6 @@ sandpaper
sandpit
sandstone
sandstorm
sandworm
sandy
sanitary
sanitizer
@ -5541,7 +5505,6 @@ satisfy
saturate
saturday
sauciness
saucy
sauna
savage
savanna
@ -5552,7 +5515,6 @@ savor
saxophone
say
scabbed
scabby
scalded
scalding
scale
@ -5587,7 +5549,6 @@ science
scientist
scion
scoff
scolding
scone
scoop
scooter
@ -5651,8 +5612,6 @@ sedate
sedation
sedative
sediment
seduce
seducing
segment
seismic
seizing
@ -5899,7 +5858,6 @@ skimpily
skincare
skinless
skinning
skinny
skintight
skipper
skipping
@ -6248,17 +6206,12 @@ stifle
stifling
stillness
stilt
stimulant
stimulate
stimuli
stimulus
stinger
stingily
stinging
stingray
stingy
stinking
stinky
stipend
stipulate
stir
@ -6866,7 +6819,6 @@ unbent
unbiased
unbitten
unblended
unblessed
unblock
unbolted
unbounded
@ -6947,7 +6899,6 @@ undertone
undertook
undertow
underuse
underwear
underwent
underwire
undesired
@ -7000,7 +6951,6 @@ unfunded
unglazed
ungloved
unglue
ungodly
ungraded
ungreased
unguarded
@ -7032,7 +6982,6 @@ uninsured
uninvited
union
uniquely
unisexual
unison
unissued
unit
@ -7493,8 +7442,6 @@ wheat
whenever
whiff
whimsical
whinny
whiny
whisking
whoever
whole
@ -7600,7 +7547,6 @@ wrongness
wrought
xbox
xerox
yahoo
yam
yanking
yapping

View File

@ -0,0 +1,102 @@
"""add missing indices for fk constraints
Revision ID: 0f3ee15b0014
Revises: 12274da2299f
Create Date: 2024-11-15 12:29:10.739938
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0f3ee15b0014'
down_revision = '12274da2299f'
branch_labels = None
depends_on = None
def upgrade():
with op.get_context().autocommit_block():
op.create_index('ix_activation_code_user_id', 'activation_code', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_alias_original_owner_id', 'alias', ['original_owner_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_alias_used_on_user_id', 'alias_used_on', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_api_to_cookie_token_api_key_id', 'api_cookie_token', ['api_key_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_api_to_cookie_token_user_id', 'api_cookie_token', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_api_key_code', 'api_key', ['code'], unique=False, postgresql_concurrently=True)
op.create_index('ix_api_key_user_id', 'api_key', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_authorization_code_client_id', 'authorization_code', ['client_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_authorization_code_user_id', 'authorization_code', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_authorized_address_user_id', 'authorized_address', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_auto_create_rule_custom_domain_id', 'auto_create_rule', ['custom_domain_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_batch_import_file_id', 'batch_import', ['file_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_batch_import_user_id', 'batch_import', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_client_icon_id', 'client', ['icon_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_client_referral_id', 'client', ['referral_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_client_user_id', 'client', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_coupon_used_by_user_id', 'coupon', ['used_by_user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_directory_user_id', 'directory', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_domain_deleted_alias_user_id', 'domain_deleted_alias', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_email_log_refused_email_id', 'email_log', ['refused_email_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_fido_user_id', 'fido', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_file_user_id', 'file', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_hibp_notified_alias_user_id', 'hibp_notified_alias', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_mfa_browser_user_id', 'mfa_browser', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_newsletter_user_user_id', 'newsletter_user', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_oauth_token_client_id', 'oauth_token', ['client_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_oauth_token_user_id', 'oauth_token', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_payout_user_id', 'payout', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_phone_reservation_user_id', 'phone_reservation', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_provider_complaint_refused_email_id', 'provider_complaint', ['refused_email_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_provider_complaint_user_id', 'provider_complaint', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_redirect_uri_client_id', 'redirect_uri', ['client_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_referral_user_id', 'referral', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_refused_email_user_id', 'refused_email', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_reset_password_code_user_id', 'reset_password_code', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_sent_alert_user_id', 'sent_alert', ['user_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_users_default_alias_custom_domain_id', 'users', ['default_alias_custom_domain_id'], unique=False, postgresql_concurrently=True)
op.create_index('ix_users_profile_picture_id', 'users', ['profile_picture_id'], unique=False, postgresql_concurrently=True)
def downgrade():
with op.get_context().autocommit_block():
op.drop_index('ix_users_profile_picture_id', table_name='users')
op.drop_index('ix_users_default_alias_custom_domain_id', table_name='users')
op.drop_index('ix_sent_alert_user_id', table_name='sent_alert')
op.drop_index('ix_reset_password_code_user_id', table_name='reset_password_code')
op.drop_index('ix_refused_email_user_id', table_name='refused_email')
op.drop_index('ix_referral_user_id', table_name='referral')
op.drop_index('ix_redirect_uri_client_id', table_name='redirect_uri')
op.drop_index('ix_provider_complaint_user_id', table_name='provider_complaint')
op.drop_index('ix_provider_complaint_refused_email_id', table_name='provider_complaint')
op.drop_index('ix_phone_reservation_user_id', table_name='phone_reservation')
op.drop_index('ix_payout_user_id', table_name='payout')
op.drop_index('ix_oauth_token_user_id', table_name='oauth_token')
op.drop_index('ix_oauth_token_client_id', table_name='oauth_token')
op.drop_index('ix_newsletter_user_user_id', table_name='newsletter_user')
op.drop_index('ix_mfa_browser_user_id', table_name='mfa_browser')
op.drop_index('ix_hibp_notified_alias_user_id', table_name='hibp_notified_alias')
op.drop_index('ix_file_user_id', table_name='file')
op.drop_index('ix_fido_user_id', table_name='fido')
op.drop_index('ix_email_log_refused_email_id', table_name='email_log')
op.drop_index('ix_domain_deleted_alias_user_id', table_name='domain_deleted_alias')
op.drop_index('ix_directory_user_id', table_name='directory')
op.drop_index('ix_coupon_used_by_user_id', table_name='coupon')
op.drop_index('ix_client_user_id', table_name='client')
op.drop_index('ix_client_referral_id', table_name='client')
op.drop_index('ix_client_icon_id', table_name='client')
op.drop_index('ix_batch_import_user_id', table_name='batch_import')
op.drop_index('ix_batch_import_file_id', table_name='batch_import')
op.drop_index('ix_auto_create_rule_custom_domain_id', table_name='auto_create_rule')
op.drop_index('ix_authorized_address_user_id', table_name='authorized_address')
op.drop_index('ix_authorization_code_user_id', table_name='authorization_code')
op.drop_index('ix_authorization_code_client_id', table_name='authorization_code')
op.drop_index('ix_api_key_user_id', table_name='api_key')
op.drop_index('ix_api_key_code', table_name='api_key')
op.drop_index('ix_api_to_cookie_token_user_id', table_name='api_cookie_token')
op.drop_index('ix_api_to_cookie_token_api_key_id', table_name='api_cookie_token')
op.drop_index('ix_alias_used_on_user_id', table_name='alias_used_on')
op.drop_index('ix_alias_original_owner_id', table_name='alias')
op.drop_index('ix_activation_code_user_id', table_name='activation_code')

View File

@ -0,0 +1,35 @@
"""empty message
Revision ID: 085f77996ce3
Revises: 0f3ee15b0014
Create Date: 2024-11-26 19:20:32.227899
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '085f77996ce3'
down_revision = '0f3ee15b0014'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('partner_subscription', sa.Column('lifetime', sa.Boolean(), server_default='0', nullable=False))
op.alter_column('partner_subscription', 'end_at',
existing_type=postgresql.TIMESTAMP(),
nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('partner_subscription', 'end_at',
existing_type=postgresql.TIMESTAMP(),
nullable=False)
op.drop_column('partner_subscription', 'lifetime')
# ### end Alembic commands ###

View File

@ -57,31 +57,21 @@
{% endblock %}
{% block default_content %}
{% if NOW.timestamp < 1701475201 %}
{% if NOW.timestamp < 1733184000 %}
<div class="alert alert-info">
Black Friday Deal: 33% off on the yearly plan for the <b>first</b> year ($20 instead of $30).
<div class="alert alert-primary">
Lifetime deal for SimpleLogin Premium and Proton Pass Plus for $199
<a class="btn btn-primary"
href="https://proton.me/pass/black-friday"
target="_blank">Buy now</a>
<br>
Please use this coupon code
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="BF2023">BF2023</em> during the checkout.
<br>
<img src="/static/images/coupon.png" class="m-2" style="max-width: 300px">
<br>
Available until December 1, 2023.
Available until December 3, 2024.
</div>
{% endif %}
<div class="pb-8">
<div class="text-center mx-md-auto mb-8 mt-6">
<div class="text-center mx-md-auto mb-4 mt-4">
<h1>Upgrade to unlock premium features</h1>
</div>
<div class="alert alert-info">
<span class="badge badge-success">new</span> SimpleLogin Premium now includes Proton Pass premium features.
<a href="https://simplelogin.io/blog/sl-premium-including-pass-plus/"
target="_blank">Learn more ↗</a>
</div>
{% if manual_sub %}
<div class="alert alert-info mt-0 mb-6">
@ -131,6 +121,11 @@
aria-selected="true">Yearly<span class="badge badge-success position-absolute tab-yearly__badge"
style="font-size: 12px">Save $18</span></a>
</div>
<div class="alert alert-info">
<span class="badge badge-success">new</span> SimpleLogin Premium now includes Proton Pass premium features.
<a href="https://simplelogin.io/blog/sl-premium-including-pass-plus/"
target="_blank">Learn more ↗</a>
</div>
<div class="tab-content mb-8">
<!-- monthly tab content -->
<div class="tab-pane"
@ -223,12 +218,12 @@
<div class="card card-md flex-grow-1">
<div class="card-body">
<div class="text-center">
<div class="h3">Proton plan</div>
<div class="h3">Proton Unlimited</div>
<div class="h3 my-3">Starts at $12.99 / month</div>
<div class="text-center mt-4 mb-6">
<a class="btn btn-lg btn-outline-primary w-100"
role="button"
href="https://account.proton.me/u/0/mail/upgrade"
href="https://account.proton.me/u/0/pass/upgrade"
target="_blank">Upgrade your Proton account</a>
</div>
</div>
@ -362,12 +357,12 @@
<div class="card card-md flex-grow-1">
<div class="card-body">
<div class="text-center">
<div class="h3">Proton plan</div>
<div class="h3">Proton Unlimited</div>
<div class="h3 my-3">Starts at $119.88 / year</div>
<div class="text-center mt-4 mb-6">
<a class="btn btn-lg btn-outline-primary w-100"
role="button"
href="https://account.proton.me/u/0/mail/upgrade"
href="https://account.proton.me/u/0/pass/upgrade"
target="_blank">Upgrade your Proton account</a>
</div>
</div>

View File

@ -79,7 +79,14 @@
</a>
</div>
{% endif %}
{% if partner_sub %}<div>Premium subscription managed by {{ partner_name }}.</div>{% endif %}
{% if partner_sub %}
{% if partner_sub.lifetime %}
<div>Premium lifetime subscription managed by {{ partner_name }}.</div>
{% else %}
<div>Premium subscription managed by {{ partner_name }}.</div>
{% endif %}
{% endif %}
{% elif current_user.in_trial() %}
Your Premium trial expires {{ current_user.trial_end | dt }}.
{% else %}

View File

@ -36,6 +36,24 @@ def test_delete_mailbox_transfer_mailbox_primary(flask_client):
assert str(mails_sent[0].msg).find("alias have been transferred") > -1
@mail_sender.store_emails_test_decorator
def test_delete_mailbox_no_email(flask_client):
user = create_new_user()
m1 = Mailbox.create(
user_id=user.id, email=random_email(), verified=True, flush=True
)
job = Job.create(
name=JOB_DELETE_MAILBOX,
payload={"mailbox_id": m1.id, "transfer_mailbox_id": None, "send_mail": False},
run_at=arrow.now(),
commit=True,
)
Session.commit()
delete_mailbox_job(job)
mails_sent = mail_sender.get_stored_emails()
assert len(mails_sent) == 0
@mail_sender.store_emails_test_decorator
def test_delete_mailbox_transfer_mailbox_in_list(flask_client):
user = create_new_user()

View File

@ -0,0 +1,100 @@
import arrow
from app.account_linking import (
SLPlan,
SLPlanType,
set_plan_for_partner_user,
)
from app.db import Session
from app.models import User, PartnerUser, PartnerSubscription
from app.proton.utils import get_proton_partner
from app.utils import random_string
from tests.utils import random_email
partner_user_id: int = 0
def setup_module():
global partner_user_id
email = random_email()
external_id = random_string()
sl_user = User.create(email, commit=True)
partner_user_id = PartnerUser.create(
user_id=sl_user.id,
partner_id=get_proton_partner().id,
external_user_id=external_id,
partner_email=email,
commit=True,
).id
def setup_function(func):
Session.query(PartnerSubscription).delete()
def test_free_plan_removes_sub():
pu = PartnerUser.get(partner_user_id)
sub_id = PartnerSubscription.create(
partner_user_id=partner_user_id,
end_at=arrow.utcnow(),
lifetime=False,
commit=True,
).id
set_plan_for_partner_user(pu, plan=SLPlan(type=SLPlanType.Free, expiration=None))
assert PartnerSubscription.get(sub_id) is None
def test_premium_plan_updates_expiration():
pu = PartnerUser.get(partner_user_id)
sub_id = PartnerSubscription.create(
partner_user_id=partner_user_id,
end_at=arrow.utcnow(),
lifetime=False,
commit=True,
).id
new_expiration = arrow.utcnow().shift(days=+10)
set_plan_for_partner_user(
pu, plan=SLPlan(type=SLPlanType.Premium, expiration=new_expiration)
)
assert PartnerSubscription.get(sub_id).end_at == new_expiration
def test_premium_plan_creates_sub():
pu = PartnerUser.get(partner_user_id)
new_expiration = arrow.utcnow().shift(days=+10)
set_plan_for_partner_user(
pu, plan=SLPlan(type=SLPlanType.Premium, expiration=new_expiration)
)
assert (
PartnerSubscription.get_by(partner_user_id=partner_user_id).end_at
== new_expiration
)
def test_lifetime_creates_sub():
pu = PartnerUser.get(partner_user_id)
new_expiration = arrow.utcnow().shift(days=+10)
set_plan_for_partner_user(
pu, plan=SLPlan(type=SLPlanType.PremiumLifetime, expiration=new_expiration)
)
sub = PartnerSubscription.get_by(partner_user_id=partner_user_id)
assert sub is not None
assert sub.end_at is None
assert sub.lifetime
def test_lifetime_updates_sub():
pu = PartnerUser.get(partner_user_id)
sub_id = PartnerSubscription.create(
partner_user_id=partner_user_id,
end_at=arrow.utcnow(),
lifetime=False,
commit=True,
).id
set_plan_for_partner_user(
pu, plan=SLPlan(type=SLPlanType.PremiumLifetime, expiration=arrow.utcnow())
)
sub = PartnerSubscription.get(sub_id)
assert sub is not None
assert sub.end_at is None
assert sub.lifetime

View File

@ -14,7 +14,6 @@ from app.email_utils import generate_verp_email
from app.mail_sender import mail_sender
from app.models import (
Alias,
AuthorizedAddress,
IgnoredEmail,
EmailLog,
Notification,
@ -24,35 +23,12 @@ from app.models import (
)
from app.utils import random_string, canonicalize_email
from email_handler import (
get_mailbox_from_mail_from,
should_ignore,
is_automatic_out_of_office,
)
from tests.utils import load_eml_file, create_new_user, random_email
def test_get_mailbox_from_mail_from(flask_client):
user = create_new_user()
alias = Alias.create_new_random(user)
Session.commit()
mb = get_mailbox_from_mail_from(user.email, alias)
assert mb.email == user.email
mb = get_mailbox_from_mail_from("unauthorized@gmail.com", alias)
assert mb is None
# authorized address
AuthorizedAddress.create(
user_id=user.id,
mailbox_id=user.default_mailbox_id,
email="unauthorized@gmail.com",
commit=True,
)
mb = get_mailbox_from_mail_from("unauthorized@gmail.com", alias)
assert mb.email == user.email
def test_should_ignore(flask_client):
assert should_ignore("mail_from", []) is False

View File

@ -791,12 +791,21 @@ def test_parse_id_from_bounce():
assert parse_id_from_bounce("anything+1234+@local") == 1234
def test_get_queue_id():
def test_get_queue_id_esmtps():
for id_type in ["SMTP", "ESMTP", "ESMTPA", "ESMTPS"]:
msg = email.message_from_string(
f"Received: from mail-wr1-x434.google.com (mail-wr1-x434.google.com [IPv6:2a00:1450:4864:20::434])\r\n\t(using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits))\r\n\t(No client certificate requested)\r\n\tby mx1.simplelogin.co (Postfix) with {id_type} id 4FxQmw1DXdz2vK2\r\n\tfor <jglfdjgld@alias.com>; Fri, 4 Jun 2021 14:55:43 +0000 (UTC)"
)
assert get_queue_id(msg) == "4FxQmw1DXdz2vK2", f"Failed for {id_type}"
def test_get_queue_id_postfix():
msg = email.message_from_string(
"Received: from mail-wr1-x434.google.com (mail-wr1-x434.google.com [IPv6:2a00:1450:4864:20::434])\r\n\t(using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits))\r\n\t(No client certificate requested)\r\n\tby mx1.simplelogin.co (Postfix) with ESMTPS id 4FxQmw1DXdz2vK2\r\n\tfor <jglfdjgld@alias.com>; Fri, 4 Jun 2021 14:55:43 +0000 (UTC)"
"Received: by mailin001.somewhere.net (Postfix)\r\n\tid 4Xz5pb2nMszGrqpL; Wed, 27 Nov 2024 17:21:59 +0000 (UTC)'] by mailin001.somewhere.net (Postfix)"
)
assert get_queue_id(msg) == "4FxQmw1DXdz2vK2"
assert get_queue_id(msg) == "4Xz5pb2nMszGrqpL"
def test_get_queue_id_from_double_header():

View File

@ -6,9 +6,18 @@ import pytest
from app import mailbox_utils, config
from app.db import Session
from app.mail_sender import mail_sender
from app.mailbox_utils import MailboxEmailChangeError
from app.models import Mailbox, MailboxActivation, User, Job, UserAuditLog
from app.mailbox_utils import MailboxEmailChangeError, get_mailbox_for_reply_phase
from app.models import (
Mailbox,
MailboxActivation,
User,
Job,
UserAuditLog,
Alias,
AuthorizedAddress,
)
from app.user_audit_log_utils import UserAuditLogAction
from app.utils import random_string, canonicalize_email
from tests.utils import create_new_user, random_email
@ -50,6 +59,14 @@ def test_already_used():
mailbox_utils.create_mailbox(user, user.email)
def test_already_used_with_different_case():
user.lifetime = True
email = random_email()
mailbox_utils.create_mailbox(user, email)
with pytest.raises(mailbox_utils.MailboxError):
mailbox_utils.create_mailbox(user, email.upper())
@mail_sender.store_emails_test_decorator
def test_create_mailbox():
email = random_email()
@ -418,3 +435,75 @@ def test_perform_mailbox_email_change_success():
user_id=user.id, action=UserAuditLogAction.UpdateMailbox.value
).count()
assert audit_log_entries == 1
def test_get_mailbox_from_mail_from(flask_client):
user = create_new_user()
alias = Alias.create_new_random(user)
Session.commit()
mb = get_mailbox_for_reply_phase(user.email, "", alias)
assert mb.email == user.email
mb = get_mailbox_for_reply_phase("unauthorized@gmail.com", "", alias)
assert mb is None
# authorized address
AuthorizedAddress.create(
user_id=user.id,
mailbox_id=user.default_mailbox_id,
email="unauthorized@gmail.com",
commit=True,
)
mb = get_mailbox_for_reply_phase("unauthorized@gmail.com", "", alias)
assert mb.email == user.email
def test_get_mailbox_from_mail_from_for_canonical_email(flask_client):
prefix = random_string(10)
email = f"{prefix}+subaddresxs@gmail.com"
canonical_email = canonicalize_email(email)
assert canonical_email != email
user = create_new_user()
mbox = Mailbox.create(
email=canonical_email, user_id=user.id, verified=True, flush=True
)
alias = Alias.create(user_id=user.id, email=random_email(), mailbox_id=mbox.id)
Session.flush()
mb = get_mailbox_for_reply_phase(email, "", alias)
assert mb.email == canonical_email
mb = get_mailbox_for_reply_phase(canonical_email, "", alias)
assert mb.email == canonical_email
def test_get_mailbox_from_mail_from_coming_from_header_if_domain_is_aligned(
flask_client,
):
domain = f"{random_string(10)}.com"
envelope_from = f"envelope_verp@{domain}"
mail_from = f"mail_from@{domain}"
user = create_new_user()
mbox = Mailbox.create(email=mail_from, user_id=user.id, verified=True, flush=True)
alias = Alias.create(user_id=user.id, email=random_email(), mailbox_id=mbox.id)
Session.flush()
mb = get_mailbox_for_reply_phase(envelope_from, mail_from, alias)
assert mb.email == mail_from
def test_get_mailbox_from_mail_from_coming_from_header_if_domain_is_not_aligned(
flask_client,
):
domain = f"{random_string(10)}.com"
envelope_from = f"envelope_verp@{domain}"
mail_from = f"mail_from@other_{domain}"
user = create_new_user()
mbox = Mailbox.create(email=mail_from, user_id=user.id, verified=True, flush=True)
alias = Alias.create(user_id=user.id, email=random_email(), mailbox_id=mbox.id)
Session.flush()
mb = get_mailbox_for_reply_phase(envelope_from, mail_from, alias)
assert mb is None