4.46.0
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m41s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 4m54s
Build-Release-Image / Merge-Images (push) Successful in 19s
Build-Release-Image / Create-Release (push) Successful in 16s
Build-Release-Image / Notify (push) Successful in 19s
All checks were successful
Build-Release-Image / Build-Image (linux/amd64) (push) Successful in 3m41s
Build-Release-Image / Build-Image (linux/arm64) (push) Successful in 4m54s
Build-Release-Image / Merge-Images (push) Successful in 19s
Build-Release-Image / Create-Release (push) Successful in 16s
Build-Release-Image / Notify (push) Successful in 19s
This commit is contained in:
parent
c67b97fe32
commit
f51d31f431
@ -34,6 +34,7 @@ from app.events.generated.event_pb2 import (
|
||||
from app.log import LOG
|
||||
from app.models import (
|
||||
Alias,
|
||||
AliasDeleteReason,
|
||||
CustomDomain,
|
||||
Directory,
|
||||
User,
|
||||
@ -309,7 +310,9 @@ def try_auto_create_via_domain(address: str) -> Optional[Alias]:
|
||||
return None
|
||||
|
||||
|
||||
def delete_alias(alias: Alias, user: User):
|
||||
def delete_alias(
|
||||
alias: Alias, user: User, reason: AliasDeleteReason = AliasDeleteReason.Unspecified
|
||||
):
|
||||
"""
|
||||
Delete an alias and add it to either global or domain trash
|
||||
Should be used instead of Alias.delete, DomainDeletedAlias.create, DeletedAlias.create
|
||||
@ -324,6 +327,7 @@ def delete_alias(alias: Alias, user: User):
|
||||
user_id=user.id,
|
||||
email=alias.email,
|
||||
domain_id=alias.custom_domain_id,
|
||||
reason=reason,
|
||||
)
|
||||
Session.add(domain_deleted_alias)
|
||||
Session.commit()
|
||||
@ -332,7 +336,7 @@ def delete_alias(alias: Alias, user: User):
|
||||
)
|
||||
else:
|
||||
if not DeletedAlias.get_by(email=alias.email):
|
||||
deleted_alias = DeletedAlias(email=alias.email)
|
||||
deleted_alias = DeletedAlias(email=alias.email, reason=reason)
|
||||
Session.add(deleted_alias)
|
||||
Session.commit()
|
||||
LOG.i(f"Moving {alias} to global trash {deleted_alias}")
|
||||
@ -453,10 +457,12 @@ def transfer_alias(alias, new_user, new_mailboxes: [Mailbox]):
|
||||
f"Alias {alias.email} has been received",
|
||||
render(
|
||||
"transactional/alias-transferred.txt",
|
||||
user=old_user,
|
||||
alias=alias,
|
||||
),
|
||||
render(
|
||||
"transactional/alias-transferred.html",
|
||||
user=old_user,
|
||||
alias=alias,
|
||||
),
|
||||
)
|
||||
|
@ -26,7 +26,7 @@ from app.errors import (
|
||||
)
|
||||
from app.extensions import limiter
|
||||
from app.log import LOG
|
||||
from app.models import Alias, Contact, Mailbox, AliasMailbox
|
||||
from app.models import Alias, Contact, Mailbox, AliasMailbox, AliasDeleteReason
|
||||
|
||||
|
||||
@deprecated
|
||||
@ -161,7 +161,7 @@ def delete_alias(alias_id):
|
||||
if not alias or alias.user_id != user.id:
|
||||
return jsonify(error="Forbidden"), 403
|
||||
|
||||
alias_utils.delete_alias(alias, user)
|
||||
alias_utils.delete_alias(alias, user, AliasDeleteReason.ManualAction)
|
||||
|
||||
return jsonify(deleted=True), 200
|
||||
|
||||
|
@ -129,8 +129,8 @@ def auth_register():
|
||||
send_email(
|
||||
email,
|
||||
"Just one more step to join SimpleLogin",
|
||||
render("transactional/code-activation.txt.jinja2", code=code),
|
||||
render("transactional/code-activation.html", code=code),
|
||||
render("transactional/code-activation.txt.jinja2", user=user, code=code),
|
||||
render("transactional/code-activation.html", user=user, code=code),
|
||||
)
|
||||
|
||||
RegisterEvent(RegisterEvent.ActionType.success, RegisterEvent.Source.api).send()
|
||||
@ -226,8 +226,8 @@ def auth_reactivate():
|
||||
send_email(
|
||||
email,
|
||||
"Just one more step to join SimpleLogin",
|
||||
render("transactional/code-activation.txt.jinja2", code=code),
|
||||
render("transactional/code-activation.html", code=code),
|
||||
render("transactional/code-activation.txt.jinja2", user=user, code=code),
|
||||
render("transactional/code-activation.html", user=user, code=code),
|
||||
)
|
||||
|
||||
return jsonify(msg="User needs to confirm their account"), 200
|
||||
|
@ -125,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, activation_link)
|
||||
email_utils.send_activation_email(user, activation_link)
|
||||
|
@ -120,7 +120,7 @@ if POSTFIX_SUBMISSION_TLS:
|
||||
else:
|
||||
default_postfix_port = 25
|
||||
POSTFIX_PORT = int(os.environ.get("POSTFIX_PORT", default_postfix_port))
|
||||
POSTFIX_TIMEOUT = os.environ.get("POSTFIX_TIMEOUT", 3)
|
||||
POSTFIX_TIMEOUT = int(os.environ.get("POSTFIX_TIMEOUT", 3))
|
||||
|
||||
# ["domain1.com", "domain2.com"]
|
||||
OTHER_ALIAS_DOMAINS = sl_getenv("OTHER_ALIAS_DOMAINS", list)
|
||||
|
@ -169,7 +169,7 @@ def send_reset_password_email(user):
|
||||
|
||||
reset_password_link = f"{URL}/auth/reset_password?code={reset_password_code.code}"
|
||||
|
||||
email_utils.send_reset_password_email(user.email, reset_password_link)
|
||||
email_utils.send_reset_password_email(user, reset_password_link)
|
||||
|
||||
|
||||
def send_change_email_confirmation(user: User, email_change: EmailChange):
|
||||
@ -179,7 +179,7 @@ def send_change_email_confirmation(user: User, email_change: EmailChange):
|
||||
|
||||
link = f"{URL}/auth/change_email?code={email_change.code}"
|
||||
|
||||
email_utils.send_change_email(email_change.new_email, user.email, link)
|
||||
email_utils.send_change_email(user, email_change.new_email, link)
|
||||
|
||||
|
||||
@dashboard_bp.route("/resend_email_change", methods=["GET", "POST"])
|
||||
|
@ -12,6 +12,7 @@ from app.extensions import limiter
|
||||
from app.log import LOG
|
||||
from app.models import (
|
||||
Alias,
|
||||
AliasDeleteReason,
|
||||
AliasGeneratorEnum,
|
||||
User,
|
||||
EmailLog,
|
||||
@ -143,7 +144,9 @@ def index():
|
||||
if request.form.get("form-name") == "delete-alias":
|
||||
LOG.i(f"User {current_user} requested deletion of alias {alias}")
|
||||
email = alias.email
|
||||
alias_utils.delete_alias(alias, current_user)
|
||||
alias_utils.delete_alias(
|
||||
alias, current_user, AliasDeleteReason.ManualAction
|
||||
)
|
||||
flash(f"Alias {email} has been deleted", "success")
|
||||
elif request.form.get("form-name") == "disable-alias":
|
||||
alias_utils.change_alias_status(alias, enabled=False)
|
||||
|
@ -33,6 +33,7 @@ from flanker.addresslib import address
|
||||
from flanker.addresslib.address import EmailAddress
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from sqlalchemy import func
|
||||
from flask_login import current_user
|
||||
|
||||
from app import config
|
||||
from app.db import Session
|
||||
@ -68,17 +69,27 @@ VERP_TIME_START = 1640995200
|
||||
VERP_HMAC_ALGO = "sha3-224"
|
||||
|
||||
|
||||
def render(template_name, **kwargs) -> str:
|
||||
def render(template_name: str, user: Optional[User], **kwargs) -> str:
|
||||
templates_dir = os.path.join(config.ROOT_DIR, "templates", "emails")
|
||||
env = Environment(loader=FileSystemLoader(templates_dir))
|
||||
|
||||
template = env.get_template(template_name)
|
||||
|
||||
if user is None:
|
||||
if current_user and current_user.is_authenticated:
|
||||
user = current_user
|
||||
|
||||
use_partner_template = False
|
||||
if user:
|
||||
use_partner_template = user.has_used_alias_from_partner()
|
||||
kwargs["user"] = user
|
||||
|
||||
return template.render(
|
||||
MAX_NB_EMAIL_FREE_PLAN=config.MAX_NB_EMAIL_FREE_PLAN,
|
||||
URL=config.URL,
|
||||
LANDING_PAGE_URL=config.LANDING_PAGE_URL,
|
||||
YEAR=arrow.now().year,
|
||||
USE_PARTNER_TEMPLATE=use_partner_template,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@ -111,53 +122,59 @@ def send_trial_end_soon_email(user):
|
||||
)
|
||||
|
||||
|
||||
def send_activation_email(email, activation_link):
|
||||
def send_activation_email(user: User, activation_link):
|
||||
send_email(
|
||||
email,
|
||||
user.email,
|
||||
"Just one more step to join SimpleLogin",
|
||||
render(
|
||||
"transactional/activation.txt",
|
||||
user=user,
|
||||
activation_link=activation_link,
|
||||
email=email,
|
||||
email=user.email,
|
||||
),
|
||||
render(
|
||||
"transactional/activation.html",
|
||||
user=user,
|
||||
activation_link=activation_link,
|
||||
email=email,
|
||||
email=user.email,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def send_reset_password_email(email, reset_password_link):
|
||||
def send_reset_password_email(user: User, reset_password_link):
|
||||
send_email(
|
||||
email,
|
||||
user.email,
|
||||
"Reset your password on SimpleLogin",
|
||||
render(
|
||||
"transactional/reset-password.txt",
|
||||
user=user,
|
||||
reset_password_link=reset_password_link,
|
||||
),
|
||||
render(
|
||||
"transactional/reset-password.html",
|
||||
user=user,
|
||||
reset_password_link=reset_password_link,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def send_change_email(new_email, current_email, link):
|
||||
def send_change_email(user: User, new_email, link):
|
||||
send_email(
|
||||
new_email,
|
||||
"Confirm email update on SimpleLogin",
|
||||
render(
|
||||
"transactional/change-email.txt",
|
||||
user=user,
|
||||
link=link,
|
||||
new_email=new_email,
|
||||
current_email=current_email,
|
||||
current_email=user.email,
|
||||
),
|
||||
render(
|
||||
"transactional/change-email.html",
|
||||
user=user,
|
||||
link=link,
|
||||
new_email=new_email,
|
||||
current_email=current_email,
|
||||
current_email=user.email,
|
||||
),
|
||||
)
|
||||
|
||||
@ -170,28 +187,32 @@ def send_invalid_totp_login_email(user, totp_type):
|
||||
"Unsuccessful attempt to login to your SimpleLogin account",
|
||||
render(
|
||||
"transactional/invalid-totp-login.txt",
|
||||
user=user,
|
||||
type=totp_type,
|
||||
),
|
||||
render(
|
||||
"transactional/invalid-totp-login.html",
|
||||
user=user,
|
||||
type=totp_type,
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
def send_test_email_alias(email, name):
|
||||
def send_test_email_alias(user: User, email: str):
|
||||
send_email(
|
||||
email,
|
||||
f"This email is sent to {email}",
|
||||
render(
|
||||
"transactional/test-email.txt",
|
||||
name=name,
|
||||
user=user,
|
||||
name=user.name,
|
||||
alias=email,
|
||||
),
|
||||
render(
|
||||
"transactional/test-email.html",
|
||||
name=name,
|
||||
user=user,
|
||||
name=user.name,
|
||||
alias=email,
|
||||
),
|
||||
)
|
||||
@ -206,11 +227,13 @@ def send_cannot_create_directory_alias(user, alias_address, directory_name):
|
||||
f"Alias {alias_address} cannot be created",
|
||||
render(
|
||||
"transactional/cannot-create-alias-directory.txt",
|
||||
user=user,
|
||||
alias=alias_address,
|
||||
directory=directory_name,
|
||||
),
|
||||
render(
|
||||
"transactional/cannot-create-alias-directory.html",
|
||||
user=user,
|
||||
alias=alias_address,
|
||||
directory=directory_name,
|
||||
),
|
||||
@ -228,11 +251,13 @@ def send_cannot_create_directory_alias_disabled(user, alias_address, directory_n
|
||||
f"Alias {alias_address} cannot be created",
|
||||
render(
|
||||
"transactional/cannot-create-alias-directory-disabled.txt",
|
||||
user=user,
|
||||
alias=alias_address,
|
||||
directory=directory_name,
|
||||
),
|
||||
render(
|
||||
"transactional/cannot-create-alias-directory-disabled.html",
|
||||
user=user,
|
||||
alias=alias_address,
|
||||
directory=directory_name,
|
||||
),
|
||||
@ -248,11 +273,13 @@ def send_cannot_create_domain_alias(user, alias, domain):
|
||||
f"Alias {alias} cannot be created",
|
||||
render(
|
||||
"transactional/cannot-create-alias-domain.txt",
|
||||
user=user,
|
||||
alias=alias,
|
||||
domain=domain,
|
||||
),
|
||||
render(
|
||||
"transactional/cannot-create-alias-domain.html",
|
||||
user=user,
|
||||
alias=alias,
|
||||
domain=domain,
|
||||
),
|
||||
@ -919,10 +946,20 @@ def decode_text(text: str, encoding: EmailEncoding = EmailEncoding.NO) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def add_header(msg: Message, text_header, html_header=None) -> Message:
|
||||
def add_header(
|
||||
msg: Message, text_header, html_header=None, subject_prefix=None
|
||||
) -> Message:
|
||||
if not html_header:
|
||||
html_header = text_header.replace("\n", "<br>")
|
||||
|
||||
if subject_prefix is not None:
|
||||
subject = msg[headers.SUBJECT]
|
||||
if not subject:
|
||||
msg.add_header(headers.SUBJECT, subject_prefix)
|
||||
else:
|
||||
subject = f"{subject_prefix} {subject}"
|
||||
msg.replace_header(headers.SUBJECT, subject)
|
||||
|
||||
content_type = msg.get_content_type().lower()
|
||||
if content_type == "text/plain":
|
||||
encoding = get_encoding(msg)
|
||||
@ -1253,6 +1290,7 @@ def spf_pass(
|
||||
f"SimpleLogin Alert: attempt to send emails from your alias {alias.email} from unknown IP Address",
|
||||
render(
|
||||
"transactional/spf-fail.txt",
|
||||
user=user,
|
||||
alias=alias.email,
|
||||
ip=ip,
|
||||
mailbox_url=config.URL + f"/dashboard/mailbox/{mailbox.id}#spf",
|
||||
@ -1262,6 +1300,7 @@ def spf_pass(
|
||||
),
|
||||
render(
|
||||
"transactional/spf-fail.html",
|
||||
user=user,
|
||||
ip=ip,
|
||||
mailbox_url=config.URL + f"/dashboard/mailbox/{mailbox.id}#spf",
|
||||
to_email=contact_email,
|
||||
|
@ -64,6 +64,7 @@ More info on https://simplelogin.io/docs/getting-started/anti-phishing/
|
||||
msg,
|
||||
warning_plain_text,
|
||||
warning_html,
|
||||
subject_prefix="[Possible phishing attempt]",
|
||||
)
|
||||
return changed_msg, None
|
||||
|
||||
@ -76,6 +77,7 @@ More info on https://simplelogin.io/docs/getting-started/anti-phishing/
|
||||
msg,
|
||||
warning_plain_text,
|
||||
warning_html,
|
||||
subject_prefix="[Possible phishing attempt]",
|
||||
)
|
||||
return changed_msg, None
|
||||
|
||||
@ -104,12 +106,14 @@ More info on https://simplelogin.io/docs/getting-started/anti-phishing/
|
||||
f"An email sent to {alias.email} has been quarantined",
|
||||
render(
|
||||
"transactional/message-quarantine-dmarc.txt.jinja2",
|
||||
user=user,
|
||||
from_header=from_header,
|
||||
alias=alias,
|
||||
refused_email_url=email_log.get_dashboard_url(),
|
||||
),
|
||||
render(
|
||||
"transactional/message-quarantine-dmarc.html",
|
||||
user=user,
|
||||
from_header=from_header,
|
||||
alias=alias,
|
||||
refused_email_url=email_log.get_dashboard_url(),
|
||||
@ -174,12 +178,14 @@ def apply_dmarc_policy_for_reply_phase(
|
||||
f"Attempt to send an email to your contact {contact_recipient.email} from {envelope.mail_from}",
|
||||
render(
|
||||
"transactional/spoof-reply.txt.jinja2",
|
||||
user=alias_from.user,
|
||||
contact=contact_recipient,
|
||||
alias=alias_from,
|
||||
sender=envelope.mail_from,
|
||||
),
|
||||
render(
|
||||
"transactional/spoof-reply.html",
|
||||
user=alias_from.user,
|
||||
contact=contact_recipient,
|
||||
alias=alias_from,
|
||||
sender=envelope.mail_from,
|
||||
|
@ -319,11 +319,13 @@ def report_complaint_to_user_in_forward_phase(
|
||||
f"Abuse report from {capitalized_name}",
|
||||
render(
|
||||
"transactional/provider-complaint-forward-phase.txt.jinja2",
|
||||
user=user,
|
||||
email=mailbox_email,
|
||||
provider=capitalized_name,
|
||||
),
|
||||
render(
|
||||
"transactional/provider-complaint-forward-phase.html",
|
||||
user=user,
|
||||
email=mailbox_email,
|
||||
provider=capitalized_name,
|
||||
),
|
||||
|
@ -137,7 +137,9 @@ class ExportUserDataJob:
|
||||
msg[headers.SUBJECT] = "Your SimpleLogin data"
|
||||
msg[headers.FROM] = f'"SimpleLogin (noreply)" <{config.NOREPLY}>'
|
||||
msg[headers.TO] = to_email
|
||||
msg.attach(MIMEText(render("transactional/user-report.html"), "html"))
|
||||
msg.attach(
|
||||
MIMEText(render("transactional/user-report.html", user=self._user), "html")
|
||||
)
|
||||
attachment = MIMEApplication(zipped_contents.read())
|
||||
attachment.add_header(
|
||||
"Content-Disposition", "attachment", filename="user_report.zip"
|
||||
|
@ -263,6 +263,15 @@ class UnsubscribeBehaviourEnum(EnumE):
|
||||
PreserveOriginal = 2
|
||||
|
||||
|
||||
class AliasDeleteReason(EnumE):
|
||||
Unspecified = 0
|
||||
UserHasBeenDeleted = 1
|
||||
ManualAction = 2
|
||||
DirectoryDeleted = 3
|
||||
MailboxDeleted = 4
|
||||
CustomDomainDeleted = 5
|
||||
|
||||
|
||||
class IntEnumType(sa.types.TypeDecorator):
|
||||
impl = sa.Integer
|
||||
|
||||
@ -330,6 +339,7 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
||||
FLAG_FREE_DISABLE_CREATE_ALIAS = 1 << 0
|
||||
FLAG_CREATED_FROM_PARTNER = 1 << 1
|
||||
FLAG_FREE_OLD_ALIAS_LIMIT = 1 << 2
|
||||
FLAG_CREATED_ALIAS_FROM_PARTNER = 1 << 3
|
||||
|
||||
email = sa.Column(sa.String(256), unique=True, nullable=False)
|
||||
|
||||
@ -666,6 +676,12 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
||||
user: User = cls.get(obj_id)
|
||||
EventDispatcher.send_event(user, EventContent(user_deleted=UserDeleted()))
|
||||
|
||||
# Manually delete all aliases for the user that is about to be deleted
|
||||
from app.alias_utils import delete_alias
|
||||
|
||||
for alias in Alias.filter_by(user_id=user.id):
|
||||
delete_alias(alias, user, AliasDeleteReason.UserHasBeenDeleted)
|
||||
|
||||
res = super(User, cls).delete(obj_id)
|
||||
if commit:
|
||||
Session.commit()
|
||||
@ -1153,6 +1169,13 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle):
|
||||
return True
|
||||
return not config.DISABLE_CREATE_CONTACTS_FOR_FREE_USERS
|
||||
|
||||
def has_used_alias_from_partner(self) -> bool:
|
||||
return (
|
||||
self.flags
|
||||
& (User.FLAG_CREATED_ALIAS_FROM_PARTNER | User.FLAG_CREATED_FROM_PARTNER)
|
||||
> 0
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User {self.id} {self.name} {self.email}>"
|
||||
|
||||
@ -1646,6 +1669,12 @@ class Alias(Base, ModelMixin):
|
||||
)
|
||||
EventDispatcher.send_event(user, EventContent(alias_created=event))
|
||||
|
||||
if (
|
||||
new_alias.flags & cls.FLAG_PARTNER_CREATED > 0
|
||||
and new_alias.user.flags & User.FLAG_CREATED_ALIAS_FROM_PARTNER == 0
|
||||
):
|
||||
user.flags = user.flags | User.FLAG_CREATED_ALIAS_FROM_PARTNER
|
||||
|
||||
if commit:
|
||||
Session.commit()
|
||||
|
||||
@ -2247,6 +2276,12 @@ class DeletedAlias(Base, ModelMixin):
|
||||
__tablename__ = "deleted_alias"
|
||||
|
||||
email = sa.Column(sa.String(256), unique=True, nullable=False)
|
||||
reason = sa.Column(
|
||||
IntEnumType(AliasDeleteReason),
|
||||
nullable=False,
|
||||
default=AliasDeleteReason.Unspecified,
|
||||
server_default=str(AliasDeleteReason.Unspecified.value),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kw):
|
||||
@ -2434,6 +2469,13 @@ class CustomDomain(Base, ModelMixin):
|
||||
if obj.is_sl_subdomain:
|
||||
DeletedSubdomain.create(domain=obj.domain)
|
||||
|
||||
from app import alias_utils
|
||||
|
||||
for alias in Alias.filter_by(custom_domain_id=obj_id):
|
||||
alias_utils.delete_alias(
|
||||
alias, obj.user, AliasDeleteReason.CustomDomainDeleted
|
||||
)
|
||||
|
||||
return super(CustomDomain, cls).delete(obj_id)
|
||||
|
||||
@property
|
||||
@ -2506,6 +2548,12 @@ class DomainDeletedAlias(Base, ModelMixin):
|
||||
|
||||
domain = orm.relationship(CustomDomain)
|
||||
user = orm.relationship(User, foreign_keys=[user_id])
|
||||
reason = sa.Column(
|
||||
IntEnumType(AliasDeleteReason),
|
||||
nullable=False,
|
||||
default=AliasDeleteReason.Unspecified,
|
||||
server_default=str(AliasDeleteReason.Unspecified.value),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kw):
|
||||
@ -2597,7 +2645,7 @@ class Directory(Base, ModelMixin):
|
||||
for alias in Alias.filter_by(directory_id=obj_id):
|
||||
from app import alias_utils
|
||||
|
||||
alias_utils.delete_alias(alias, user)
|
||||
alias_utils.delete_alias(alias, user, AliasDeleteReason.DirectoryDeleted)
|
||||
|
||||
DeletedDirectory.create(name=obj.name)
|
||||
cls.filter(cls.id == obj_id).delete()
|
||||
@ -2725,7 +2773,7 @@ class Mailbox(Base, ModelMixin):
|
||||
from app import alias_utils
|
||||
|
||||
# only put aliases that have mailbox as a single mailbox into trash
|
||||
alias_utils.delete_alias(alias, user)
|
||||
alias_utils.delete_alias(alias, user, AliasDeleteReason.MailboxDeleted)
|
||||
Session.commit()
|
||||
|
||||
cls.filter(cls.id == obj_id).delete()
|
||||
@ -2971,11 +3019,7 @@ class RecoveryCode(Base, ModelMixin):
|
||||
@classmethod
|
||||
def find_by_user_code(cls, user: User, code: str):
|
||||
hashed_code = cls._hash_code(code)
|
||||
# TODO: Only return hashed codes once there aren't unhashed codes in the db.
|
||||
found_code = cls.get_by(user_id=user.id, code=hashed_code)
|
||||
if found_code:
|
||||
return found_code
|
||||
return cls.get_by(user_id=user.id, code=code)
|
||||
return cls.get_by(user_id=user.id, code=hashed_code)
|
||||
|
||||
@classmethod
|
||||
def empty(cls, user):
|
||||
|
@ -20,7 +20,7 @@ def final():
|
||||
if form.validate_on_submit():
|
||||
alias = Alias.get_by(email=form.email.data)
|
||||
if alias and alias.user_id == current_user.id:
|
||||
send_test_email_alias(alias.email, current_user.name)
|
||||
send_test_email_alias(current_user, alias.email)
|
||||
flash("An email is sent to your alias", "success")
|
||||
|
||||
return render_template(
|
||||
|
@ -27,6 +27,7 @@ def failed_payment(sub: Subscription, subscription_id: str):
|
||||
"SimpleLogin - your subscription has failed to be renewed",
|
||||
render(
|
||||
"transactional/subscription-cancel.txt",
|
||||
user=user,
|
||||
end_date=arrow.arrow.datetime.utcnow(),
|
||||
),
|
||||
)
|
||||
|
@ -266,11 +266,13 @@ def notify_manual_sub_end():
|
||||
"Your SimpleLogin subscription will end soon",
|
||||
render(
|
||||
"transactional/coinbase/reminder-subscription.txt",
|
||||
user=user,
|
||||
coinbase_subscription=coinbase_subscription,
|
||||
extend_subscription_url=extend_subscription_url,
|
||||
),
|
||||
render(
|
||||
"transactional/coinbase/reminder-subscription.html",
|
||||
user=user,
|
||||
coinbase_subscription=coinbase_subscription,
|
||||
extend_subscription_url=extend_subscription_url,
|
||||
),
|
||||
@ -826,10 +828,12 @@ def check_mailbox_valid_domain():
|
||||
f"Mailbox {mailbox.email} is disabled",
|
||||
render(
|
||||
"transactional/disable-mailbox-warning.txt.jinja2",
|
||||
user=mailbox.user,
|
||||
mailbox=mailbox,
|
||||
),
|
||||
render(
|
||||
"transactional/disable-mailbox-warning.html",
|
||||
user=mailbox.user,
|
||||
mailbox=mailbox,
|
||||
),
|
||||
retries=3,
|
||||
@ -884,6 +888,7 @@ def check_mailbox_valid_pgp_keys():
|
||||
f"Mailbox {mailbox.email}'s PGP Key is invalid",
|
||||
render(
|
||||
"transactional/invalid-mailbox-pgp-key.txt.jinja2",
|
||||
user=mailbox.user,
|
||||
mailbox=mailbox,
|
||||
),
|
||||
retries=3,
|
||||
@ -924,6 +929,7 @@ def check_single_custom_domain(custom_domain):
|
||||
f"Please update {custom_domain.domain} DNS on SimpleLogin",
|
||||
render(
|
||||
"transactional/custom-domain-dns-issue.txt.jinja2",
|
||||
user=user,
|
||||
custom_domain=custom_domain,
|
||||
domain_dns_url=domain_dns_url,
|
||||
),
|
||||
|
@ -601,12 +601,14 @@ def handle_email_sent_to_ourself(alias, from_addr: str, msg: Message, user):
|
||||
f"Email sent to {alias.email} from its own mailbox {from_addr}",
|
||||
render(
|
||||
"transactional/cycle-email.txt.jinja2",
|
||||
user=user,
|
||||
alias=alias,
|
||||
from_addr=from_addr,
|
||||
refused_email_url=refused_email_url,
|
||||
),
|
||||
render(
|
||||
"transactional/cycle-email.html",
|
||||
user=user,
|
||||
alias=alias,
|
||||
from_addr=from_addr,
|
||||
refused_email_url=refused_email_url,
|
||||
@ -728,12 +730,14 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str
|
||||
f"Your mailbox {mailbox.email} is an alias",
|
||||
render(
|
||||
"transactional/mailbox-invalid.txt.jinja2",
|
||||
user=mailbox.user,
|
||||
mailbox=mailbox,
|
||||
mailbox_url=mailbox_url,
|
||||
alias=alias,
|
||||
),
|
||||
render(
|
||||
"transactional/mailbox-invalid.html",
|
||||
user=mailbox.user,
|
||||
mailbox=mailbox,
|
||||
mailbox_url=mailbox_url,
|
||||
alias=alias,
|
||||
@ -786,12 +790,14 @@ def forward_email_to_mailbox(
|
||||
f"Your mailbox {mailbox.email} and alias {alias.email} use the same domain",
|
||||
render(
|
||||
"transactional/mailbox-invalid.txt.jinja2",
|
||||
user=mailbox.user,
|
||||
mailbox=mailbox,
|
||||
mailbox_url=mailbox_url,
|
||||
alias=alias,
|
||||
),
|
||||
render(
|
||||
"transactional/mailbox-invalid.html",
|
||||
user=mailbox.user,
|
||||
mailbox=mailbox,
|
||||
mailbox_url=mailbox_url,
|
||||
alias=alias,
|
||||
@ -1276,6 +1282,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
|
||||
f"Email sent to {contact.email} contains non reverse-alias addresses",
|
||||
render(
|
||||
"transactional/non-reverse-alias-reply-phase.txt.jinja2",
|
||||
user=alias.user,
|
||||
destination=contact.email,
|
||||
alias=alias.email,
|
||||
subject=msg[headers.SUBJECT],
|
||||
@ -1497,6 +1504,7 @@ def handle_unknown_mailbox(
|
||||
f"Attempt to use your alias {alias.email} from {envelope.mail_from}",
|
||||
render(
|
||||
"transactional/reply-must-use-personal-email.txt",
|
||||
user=user,
|
||||
alias=alias,
|
||||
sender=envelope.mail_from,
|
||||
authorize_address_link=authorize_address_link,
|
||||
@ -1504,6 +1512,7 @@ def handle_unknown_mailbox(
|
||||
),
|
||||
render(
|
||||
"transactional/reply-must-use-personal-email.html",
|
||||
user=user,
|
||||
alias=alias,
|
||||
sender=envelope.mail_from,
|
||||
authorize_address_link=authorize_address_link,
|
||||
@ -1604,12 +1613,14 @@ def handle_bounce_forward_phase(msg: Message, email_log: EmailLog):
|
||||
f"Alias {alias.email} has been disabled due to multiple bounces",
|
||||
render(
|
||||
"transactional/bounce/automatic-disable-alias.txt",
|
||||
user=alias.user,
|
||||
alias=alias,
|
||||
refused_email_url=refused_email_url,
|
||||
mailbox_email=mailbox.email,
|
||||
),
|
||||
render(
|
||||
"transactional/bounce/automatic-disable-alias.html",
|
||||
user=alias.user,
|
||||
alias=alias,
|
||||
refused_email_url=refused_email_url,
|
||||
mailbox_email=mailbox.email,
|
||||
@ -1648,6 +1659,7 @@ def handle_bounce_forward_phase(msg: Message, email_log: EmailLog):
|
||||
f"An email sent to {alias.email} cannot be delivered to your mailbox",
|
||||
render(
|
||||
"transactional/bounce/bounced-email.txt.jinja2",
|
||||
user=alias.user,
|
||||
alias=alias,
|
||||
website_email=contact.website_email,
|
||||
disable_alias_link=disable_alias_link,
|
||||
@ -1657,6 +1669,7 @@ def handle_bounce_forward_phase(msg: Message, email_log: EmailLog):
|
||||
),
|
||||
render(
|
||||
"transactional/bounce/bounced-email.html",
|
||||
user=alias.user,
|
||||
alias=alias,
|
||||
website_email=contact.website_email,
|
||||
disable_alias_link=disable_alias_link,
|
||||
@ -1749,12 +1762,14 @@ def handle_bounce_reply_phase(envelope, msg: Message, email_log: EmailLog):
|
||||
f"Email cannot be sent to { contact.email } from your alias { alias.email }",
|
||||
render(
|
||||
"transactional/bounce/bounce-email-reply-phase.txt",
|
||||
user=user,
|
||||
alias=alias,
|
||||
contact=contact,
|
||||
refused_email_url=refused_email_url,
|
||||
),
|
||||
render(
|
||||
"transactional/bounce/bounce-email-reply-phase.html",
|
||||
user=user,
|
||||
alias=alias,
|
||||
contact=contact,
|
||||
refused_email_url=refused_email_url,
|
||||
@ -1817,6 +1832,7 @@ def handle_spam(
|
||||
f"Email from {alias.email} to {contact.website_email} is detected as spam",
|
||||
render(
|
||||
"transactional/spam-email-reply-phase.txt",
|
||||
user=user,
|
||||
alias=alias,
|
||||
website_email=contact.website_email,
|
||||
disable_alias_link=disable_alias_link,
|
||||
@ -1824,6 +1840,7 @@ def handle_spam(
|
||||
),
|
||||
render(
|
||||
"transactional/spam-email-reply-phase.html",
|
||||
user=user,
|
||||
alias=alias,
|
||||
website_email=contact.website_email,
|
||||
disable_alias_link=disable_alias_link,
|
||||
@ -1846,6 +1863,7 @@ def handle_spam(
|
||||
f"Email from {contact.website_email} to {alias.email} is detected as spam",
|
||||
render(
|
||||
"transactional/spam-email.txt",
|
||||
user=user,
|
||||
alias=alias,
|
||||
website_email=contact.website_email,
|
||||
disable_alias_link=disable_alias_link,
|
||||
@ -1853,6 +1871,7 @@ def handle_spam(
|
||||
),
|
||||
render(
|
||||
"transactional/spam-email.html",
|
||||
user=user,
|
||||
alias=alias,
|
||||
website_email=contact.website_email,
|
||||
disable_alias_link=disable_alias_link,
|
||||
@ -2009,7 +2028,7 @@ def send_no_reply_response(mail_from: str, msg: Message):
|
||||
ALERT_TO_NOREPLY,
|
||||
mailbox.user.email,
|
||||
"Auto: {}".format(msg[headers.SUBJECT] or "No subject"),
|
||||
render("transactional/noreply.text.jinja2"),
|
||||
render("transactional/noreply.text.jinja2", user=mailbox.user),
|
||||
)
|
||||
|
||||
|
||||
@ -2091,6 +2110,7 @@ def handle(envelope: Envelope, msg: Message) -> str:
|
||||
"SimpleLogin shouldn't be used with another email forwarding system",
|
||||
render(
|
||||
"transactional/email-sent-from-reverse-alias.txt.jinja2",
|
||||
user=user,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -225,16 +225,15 @@ def process_job(job: Job):
|
||||
|
||||
user_email = user.email
|
||||
LOG.w("Delete user %s", user)
|
||||
User.delete(user.id)
|
||||
Session.commit()
|
||||
|
||||
send_email(
|
||||
user_email,
|
||||
"Your SimpleLogin account has been deleted",
|
||||
render("transactional/account-delete.txt"),
|
||||
render("transactional/account-delete.html"),
|
||||
render("transactional/account-delete.txt", user=user),
|
||||
render("transactional/account-delete.html", user=user),
|
||||
retries=3,
|
||||
)
|
||||
User.delete(user.id)
|
||||
Session.commit()
|
||||
elif job.name == config.JOB_DELETE_MAILBOX:
|
||||
delete_mailbox_job(job)
|
||||
|
||||
|
31
app/migrations/versions/2024_070516_d608b8e48082_.py
Normal file
31
app/migrations/versions/2024_070516_d608b8e48082_.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: d608b8e48082
|
||||
Revises: 06a9a7133445
|
||||
Create Date: 2024-07-05 16:56:04.220173
|
||||
|
||||
"""
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd608b8e48082'
|
||||
down_revision = '06a9a7133445'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('deleted_alias', sa.Column('reason', sa.Integer(), default=0, server_default='0', nullable=False))
|
||||
op.add_column('domain_deleted_alias', sa.Column('reason', sa.Integer(), default=0, server_default='0', nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('domain_deleted_alias', 'reason')
|
||||
op.drop_column('deleted_alias', 'reason')
|
||||
# ### end Alembic commands ###
|
@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import time
|
||||
|
||||
from sqlalchemy import func
|
||||
from app.models import Alias, User
|
||||
from app.db import Session
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="Backfill alias", description="Backfill user flags for partner alias created"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s", "--start_user_id", default=0, type=int, help="Initial user_id"
|
||||
)
|
||||
parser.add_argument("-e", "--end_user_id", default=0, type=int, help="Last user_id")
|
||||
|
||||
args = parser.parse_args()
|
||||
user_id_start = args.start_user_id
|
||||
max_user_id = args.end_user_id
|
||||
if max_user_id == 0:
|
||||
max_user_id = Session.query(func.max(User.id)).scalar()
|
||||
|
||||
print(f"Checking user {user_id_start} to {max_user_id}")
|
||||
step = 1000
|
||||
el_query = "SELECT user_id, count(id) from alias where user_id>=:start AND user_id < :end AND flags & :alias_flag > 0 GROUP BY user_id"
|
||||
user_update_query = "UPDATE users set flags = flags | :user_flag where id = :user_id"
|
||||
updated = 0
|
||||
start_time = time.time()
|
||||
for batch_start in range(user_id_start, max_user_id, step):
|
||||
rows = Session.execute(
|
||||
el_query,
|
||||
{
|
||||
"start": batch_start,
|
||||
"end": batch_start + step,
|
||||
"alias_flag": Alias.FLAG_PARTNER_CREATED,
|
||||
},
|
||||
)
|
||||
for row in rows:
|
||||
if row[1] > 0:
|
||||
Session.execute(
|
||||
user_update_query,
|
||||
{"user_id": row[0], "user_flag": User.FLAG_CREATED_ALIAS_FROM_PARTNER},
|
||||
)
|
||||
Session.commit()
|
||||
updated += 1
|
||||
elapsed = time.time() - start_time
|
||||
time_per_alias = elapsed / (updated + 1)
|
||||
last_batch_id = batch_start + step
|
||||
remaining = max_user_id - last_batch_id
|
||||
time_remaining = (max_user_id - last_batch_id) * time_per_alias
|
||||
hours_remaining = time_remaining / 3600.0
|
||||
print(
|
||||
f"\rUser {batch_start}/{max_user_id} {updated} {hours_remaining:.2f}hrs remaining"
|
||||
)
|
||||
print("")
|
@ -542,6 +542,7 @@ def setup_paddle_callback(app: Flask):
|
||||
"SimpleLogin - your subscription is canceled",
|
||||
render(
|
||||
"transactional/subscription-cancel.txt",
|
||||
user=user,
|
||||
end_date=request.form.get("cancellation_effective_date"),
|
||||
),
|
||||
)
|
||||
@ -722,10 +723,12 @@ def handle_coinbase_event(event) -> bool:
|
||||
"Your SimpleLogin account has been upgraded",
|
||||
render(
|
||||
"transactional/coinbase/new-subscription.txt",
|
||||
user=user,
|
||||
coinbase_subscription=coinbase_subscription,
|
||||
),
|
||||
render(
|
||||
"transactional/coinbase/new-subscription.html",
|
||||
user=user,
|
||||
coinbase_subscription=coinbase_subscription,
|
||||
),
|
||||
)
|
||||
@ -746,10 +749,12 @@ def handle_coinbase_event(event) -> bool:
|
||||
"Your SimpleLogin account has been extended",
|
||||
render(
|
||||
"transactional/coinbase/extend-subscription.txt",
|
||||
user=user,
|
||||
coinbase_subscription=coinbase_subscription,
|
||||
),
|
||||
render(
|
||||
"transactional/coinbase/extend-subscription.html",
|
||||
user=user,
|
||||
coinbase_subscription=coinbase_subscription,
|
||||
),
|
||||
)
|
||||
|
BIN
app/static/logo-proton.png
vendored
Normal file
BIN
app/static/logo-proton.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
@ -1,623 +1,8 @@
|
||||
{% from "_emailhelpers.html" import render_text, text, render_button, raw_url, grey_section, section %}
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
line-height: 1.6;
|
||||
}
|
||||
{% if USE_PARTNER_TEMPLATE %}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
{% extends "base_partner.html" %}
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
{% else %}
|
||||
{% extends "base_sl.html" %}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin: .4em 0 1.1875em;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* Utilities ------------------------------ */
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
/* Buttons ------------------------------ */
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
border-right: 18px solid #3869D4;
|
||||
border-bottom: 10px solid #3869D4;
|
||||
border-left: 18px solid #3869D4;
|
||||
display: inline-block;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
border-right: 18px solid #22BC66;
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
border-right: 18px solid #FF6136;
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
/* Attribute list ------------------------------ */
|
||||
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
/* Related Items ------------------------------ */
|
||||
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
/* Discount Code ------------------------------ */
|
||||
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
/* Social Icons ------------------------------ */
|
||||
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
/* Data table ------------------------------ */
|
||||
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 35px 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
/* Masthead ----------------------- */
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #A8AAAF;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
/* Body ------------------------------ */
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.email-body_inner {
|
||||
width: 750px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 750px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
width: 750px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 750px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
padding: 30px;
|
||||
}
|
||||
/*Media Queries ------------------------------ */
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body,
|
||||
.email-body,
|
||||
.email-body_inner,
|
||||
.email-content,
|
||||
.email-wrapper,
|
||||
.email-masthead,
|
||||
.email-footer {
|
||||
background-color: #333333 !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
color: #FFF !important;
|
||||
}
|
||||
.attributes_content,
|
||||
.discount {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
.email-masthead_name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="width: 100% !important;
|
||||
height: 100%;
|
||||
-webkit-text-size-adjust: none;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
margin: 0;"
|
||||
bgcolor="#F2F4F6">
|
||||
<span class="preheader"
|
||||
style="display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;">{{ pre_header }}</span>
|
||||
<table class="email-wrapper"
|
||||
width="100%"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 100%;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
margin: 0;
|
||||
padding: 0;"
|
||||
bgcolor="#F2F4F6">
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;">
|
||||
<table class="email-content"
|
||||
width="100%"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 100%;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
margin: 0;
|
||||
padding: 0;">
|
||||
<tr>
|
||||
<td class="email-masthead"
|
||||
style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
padding: 25px 0;"
|
||||
align="center">
|
||||
<a href="{{ LANDING_PAGE_URL }}"
|
||||
class="f-fallback email-masthead_name"
|
||||
style="color: #A8AAAF;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;">
|
||||
{% block logo %}<img src="{{ URL }}/static/logo.png" style="width: 150px; margin: auto">{% endblock %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Email Body -->
|
||||
<tr>
|
||||
<td class="email-body"
|
||||
width="750"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="word-break: break-word;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;">
|
||||
<table class="email-body_inner"
|
||||
align="center"
|
||||
width="750"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 750px;
|
||||
-premailer-width: 750px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
margin: 0 auto;
|
||||
padding: 0;"
|
||||
bgcolor="#FFFFFF">
|
||||
<!-- Body content -->
|
||||
<tr>
|
||||
<td class="content-cell"
|
||||
style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
padding: 30px;">
|
||||
<div class="f-fallback">
|
||||
{% block greeting %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
<!-- Sub copy -->
|
||||
{% block sub_copy %}{% endblock %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;">
|
||||
<table class="email-footer"
|
||||
align="center"
|
||||
width="750"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 750px;
|
||||
-premailer-width: 750px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
padding: 0;">
|
||||
<tr>
|
||||
<td class="content-cell"
|
||||
align="center"
|
||||
style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
padding: 30px;">
|
||||
<p class="f-fallback sub align-center"
|
||||
style="font-size: 13px;
|
||||
line-height: 1.625;
|
||||
text-align: center;
|
||||
color: #A8AAAF;
|
||||
margin: .4em 0 1.1875em;"
|
||||
align="center">
|
||||
© {{ YEAR }} SimpleLogin - a Proton product. All rights reserved.
|
||||
<br />
|
||||
{% block footer %}{% endblock %}
|
||||
</p>
|
||||
{% if unsubscribe_oneclick is defined %}
|
||||
|
||||
<p class="f-fallback sub align-center"
|
||||
style="font-size: 13px;
|
||||
line-height: 1.625;
|
||||
text-align: center;
|
||||
margin: .4em 0 1.1875em;">
|
||||
<a href="{{ unsubscribe_oneclick }}">Unsubscribe from our newsletter</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="f-fallback sub align-center"
|
||||
style="font-size: 13px;
|
||||
line-height: 1.625;
|
||||
text-align: center;
|
||||
color: #A8AAAF;
|
||||
margin: .4em 0 1.1875em;"
|
||||
align="center">
|
||||
<a href="https://app.simplelogin.io/dashboard/support">Do you have a question?</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
{% endif %}
|
||||
|
646
app/templates/emails/base_partner.html
Normal file
646
app/templates/emails/base_partner.html
Normal file
@ -0,0 +1,646 @@
|
||||
{% from "_emailhelpers.html" import render_text, text, render_button, raw_url, grey_section, section %}
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:v="urn:schemas-microsoft-com:vml"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<!-- NAME: 1 COLUMN -->
|
||||
<!--[if gte mso 15]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<meta charset="UTF-8">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta name="format-detection"
|
||||
content="telephone=no, date=no, address=no, email=no, url=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="author" content="Proton">
|
||||
<style type="text/css">
|
||||
p {
|
||||
margin: 12px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img,
|
||||
a img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
body,
|
||||
#bodyTable,
|
||||
#bodyCell {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mcnPreviewText {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
table {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
.ReadMsgBody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
p,
|
||||
a,
|
||||
li,
|
||||
td,
|
||||
blockquote {
|
||||
mso-line-height-rule: exactly;
|
||||
}
|
||||
|
||||
a[href^=tel],
|
||||
a[href^=sms] {
|
||||
color: inherit;
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
p,
|
||||
a,
|
||||
li,
|
||||
td,
|
||||
body,
|
||||
table,
|
||||
blockquote {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.no-link a,
|
||||
a[x-apple-data-detectors],
|
||||
a[href^="x-apple-data-detectors:"] {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
font-size: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
}
|
||||
|
||||
#bodyCell {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.templateContainer {
|
||||
max-width: 600px !important;
|
||||
}
|
||||
|
||||
a.mcnButton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mcnImage,
|
||||
.mcnRetinaImage {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.mcnTextContent {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.mcnTextContent img {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.mcnDividerBlock {
|
||||
table-layout: fixed !important;
|
||||
}
|
||||
|
||||
.mcnHalfTextRight {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
@media only screen and (min-width:768px) {
|
||||
.templateContainer {
|
||||
width: 600px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
|
||||
body,
|
||||
table,
|
||||
td,
|
||||
p,
|
||||
a,
|
||||
li,
|
||||
blockquote {
|
||||
-webkit-text-size-adjust: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.mcnRetinaImage {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.mcnImage {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.mcnCaptionLeftImageContent .mcnImage,
|
||||
.mcnCaptionRightImageContent .mcnImage {
|
||||
width: 176px !important;
|
||||
}
|
||||
|
||||
.mcnHalfCaptionLeftImageContent .mcnImage,
|
||||
.mcnHalfCaptionRightImageContent .mcnImage {
|
||||
width: 268px !important;
|
||||
}
|
||||
|
||||
.mcnBoxContentColumnBoxed {
|
||||
padding: 8px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.mcnButtonContentContainer,
|
||||
.mcnCartContainer,
|
||||
.mcnCaptionTopContent,
|
||||
.mcnRecContentContainer,
|
||||
.mcnCaptionBottomContent,
|
||||
.mcnTextContentContainer,
|
||||
.mcnBoxedTextContentContainer,
|
||||
.mcnImageGroupContentContainer,
|
||||
.mcnCaptionLeftTextContentContainer,
|
||||
.mcnCaptionRightTextContentContainer,
|
||||
.mcnCaptionLeftImageContentContainer,
|
||||
.mcnCaptionRightImageContentContainer,
|
||||
.mcnImageCardLeftTextContentContainer,
|
||||
.mcnImageCardRightTextContentContainer,
|
||||
.mcnImageCardLeftImageContentContainer,
|
||||
.mcnImageCardRightImageContentContainer {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.mcnBoxedTextContentContainer {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.mcnImageGroupContent {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.mcnCaptionLeftContentOuter .mcnTextContent,
|
||||
.mcnCaptionRightContentOuter .mcnTextContent {
|
||||
padding-top: 16px !important;
|
||||
}
|
||||
|
||||
.mcnImageCardTopImageContent,
|
||||
.mcnCaptionBottomContent:last-child .mcnCaptionBottomImageContent,
|
||||
.mcnCaptionBlockInner .mcnCaptionTopContent:last-child .mcnTextContent {
|
||||
padding-top: 32px !important;
|
||||
}
|
||||
|
||||
.mcnHalfCaptionLeftImageContent,
|
||||
.mcnHalfCaptionRightImageContent {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mcnImageCardBottomImageContent {
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
|
||||
.mcnImageGroupBlockInner {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.mcnImageGroupBlockOuter {
|
||||
padding-top: 16px !important;
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
|
||||
.mcnTextContent,
|
||||
.mcnBoxedTextContentColumn {
|
||||
padding-right: 32px !important;
|
||||
padding-left: 32px !important;
|
||||
}
|
||||
|
||||
.mcnCaptionBottomContent .mcnTextContent {
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important;
|
||||
}
|
||||
|
||||
.mcnCaptionLeftTextContentContainer .mcnTextContent,
|
||||
.mcnCaptionRightTextContentContainer .mcnTextContent {
|
||||
padding-right: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.mcnImageCardLeftImageContent,
|
||||
.mcnImageCardRightImageContent {
|
||||
padding-right: 32px !important;
|
||||
padding-bottom: 0 !important;
|
||||
padding-left: 32px !important;
|
||||
}
|
||||
|
||||
.mcnTextContent ul,
|
||||
.mcnTextContent ol {
|
||||
padding-inline-start: 24px !important;
|
||||
}
|
||||
|
||||
.mcnButtonContent {
|
||||
padding-left: 24px !important;
|
||||
padding-right: 24px !important;
|
||||
}
|
||||
|
||||
.mcnButtonHint {
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important;
|
||||
}
|
||||
|
||||
.mcnButtonHint,
|
||||
.mcnButtonHint * {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.mcpreview-image-uploader {
|
||||
display: none !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.hide-on-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.flex-stack-on-mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-stack-on-mobile .mcnCaptionBottomContent,
|
||||
.flex-stack-on-mobile .mcnBoxContentContainer {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Heading 1
|
||||
@tip Make the first-level headings larger in size for better readability on small screens.
|
||||
*/
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
line-height: 1.25em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Heading 2
|
||||
@tip Make the second-level headings larger in size for better readability on small screens.
|
||||
*/
|
||||
h2 {
|
||||
font-size: 20px !important;
|
||||
line-height: 1.25em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Heading 3
|
||||
@tip Make the third-level headings larger in size for better readability on small screens.
|
||||
*/
|
||||
h3 {
|
||||
font-size: 18px !important;
|
||||
line-height: 1.25em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Heading 4
|
||||
@tip Make the fourth-level headings larger in size for better readability on small screens.
|
||||
*/
|
||||
h4 {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.5em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Boxed Text
|
||||
@tip Make the boxed text larger in size for better readability on small screens. We recommend a font size of at least 16px.
|
||||
*/
|
||||
.mcnBoxedTextContentContainer .mcnTextContent,
|
||||
.mcnBoxedTextContentContainer .mcnTextContent p {
|
||||
font-size: 14px !important;
|
||||
line-height: 1.5em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Preheader Visibility
|
||||
@tip Set the visibility of the email's preheader on small screens. You can hide it to save space.
|
||||
*/
|
||||
#templatePreheader {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Preheader Text
|
||||
@tip Make the preheader text larger in size for better readability on small screens.
|
||||
*/
|
||||
#templatePreheader .mcnTextContent,
|
||||
#templatePreheader .mcnTextContent p,
|
||||
#templateBody .templatePreheader .mcnTextContent,
|
||||
#templateBody .templatePreheader .mcnTextContent p {
|
||||
font-size: 13px !important;
|
||||
line-height: 1.5em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Header Text
|
||||
@tip Make the header text larger in size for better readability on small screens.
|
||||
*/
|
||||
#templateHeader .mcnTextContent,
|
||||
#templateHeader .mcnTextContent p {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.5em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Body Text
|
||||
@tip Make the body text larger in size for better readability on small screens. We recommend a font size of at least 16px.
|
||||
*/
|
||||
#templateBody .mcnTextContent,
|
||||
#templateBody .mcnTextContent p {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.5em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Body Caption Text
|
||||
@tip Make the body text larger in size for better readability on small screens. We recommend a font size of at least 16px.
|
||||
*/
|
||||
#templateBody .templateBodyCaption,
|
||||
#templateBody .templateBodyCaption p {
|
||||
font-size: 14px !important;
|
||||
line-height: 1.5em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Footer Text
|
||||
@tip Make the footer content text larger in size for better readability on small screens.
|
||||
*/
|
||||
#templateFooter .mcnTextContent,
|
||||
#templateFooter .mcnTextContent p {
|
||||
font-size: 14px !important;
|
||||
line-height: 1.5em !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Footer Follow icons
|
||||
@tip Reduce the spacing between the footer icons to avoid a line-break them on small screens.
|
||||
*/
|
||||
#templateFooter .mcnFollowContentItemContainer {
|
||||
padding-left: 2px !important;
|
||||
padding-right: 2px !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Footer Follow icons
|
||||
@tip Reduce the spacing between the footer icons to avoid a line-break them on small screens.
|
||||
*/
|
||||
#templateFooter .mcnFollowContentItemContainerSmall {
|
||||
padding-left: 8px !important;
|
||||
padding-right: 8px !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@text Mobile Style
|
||||
*/
|
||||
.mcnCaptionRightImageContent,
|
||||
.mcnCaptionLeftImageContent {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 352px) {
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Footer Follow icons
|
||||
@tip Reduce the icon size on very small screens.
|
||||
*/
|
||||
.mcnFollowIconContent,
|
||||
.mcnFollowIconContent img.social-icon {
|
||||
width: 38px !important;
|
||||
height: 38px !important;
|
||||
}
|
||||
|
||||
/*
|
||||
@tab Mobile Styles
|
||||
@section Footer Follow icons
|
||||
@tip Remove the spacing between the footer icons to avoid a line-break them on very small screens.
|
||||
*/
|
||||
#templateFooter .mcnFollowContentItemContainer {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;">
|
||||
<!--[if !gte mso 9]><!----><span class="mcnPreviewText"
|
||||
style="display:none;
|
||||
font-size:0px;
|
||||
line-height:0px;
|
||||
max-height:0px;
|
||||
max-width:0px;
|
||||
opacity:0;
|
||||
overflow:hidden;
|
||||
visibility:hidden;
|
||||
mso-hide:all;"></span><!--<![endif]-->
|
||||
<center>
|
||||
<table align="center"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
height="100%"
|
||||
width="100%"
|
||||
id="bodyTable"
|
||||
style="border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;">
|
||||
<tr>
|
||||
<td align="center"
|
||||
valign="top"
|
||||
id="bodyCell"
|
||||
style="mso-line-height-rule: exactly;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
width: 100%;">
|
||||
<!-- BEGIN TEMPLATE // -->
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600" style="width:600px;"><tr><td align="center" valign="top" width="600" style="width:600px;">
|
||||
<![endif]-->
|
||||
<table border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
width="100%"
|
||||
class="templateContainer"
|
||||
style="border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
max-width: 600px !important;">
|
||||
<tr>
|
||||
<td valign="top"
|
||||
id="templateHeader"
|
||||
style="mso-line-height-rule: exactly;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;">
|
||||
<table border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
width="100%"
|
||||
class="mcnImageBlock"
|
||||
style="min-width: 100%;
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;">
|
||||
<tbody class="mcnImageBlockOuter">
|
||||
<tr>
|
||||
<td valign="top"
|
||||
style="padding: 16px;
|
||||
mso-line-height-rule: exactly;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;"
|
||||
class="mcnImageBlockInner">
|
||||
<table align="left"
|
||||
width="100%"
|
||||
border="0"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
class="mcnImageContentContainer"
|
||||
style="min-width: 100%;
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="mcnImageContent"
|
||||
valign="top"
|
||||
style="padding: 16px;
|
||||
text-align: center;
|
||||
mso-line-height-rule: exactly;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;">
|
||||
<a href="https://proton.me/" target="_blank" style="">
|
||||
<img align="center"
|
||||
alt="Proton"
|
||||
src="{{ URL }}/static/logo-proton.png"
|
||||
width="190"
|
||||
style="width:35.4477%; max-width: 380px; padding-bottom: 0; display: inline !important; vertical-align: bottom; border: 0; height: auto; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; ">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top"
|
||||
id="templateBody"
|
||||
style="mso-line-height-rule: exactly; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; ">
|
||||
{% block greeting %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
<!-- Sub copy -->
|
||||
{% block sub_copy %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
<!-- // END TEMPLATE -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</center>
|
||||
</body>
|
||||
</html>
|
623
app/templates/emails/base_sl.html
Normal file
623
app/templates/emails/base_sl.html
Normal file
@ -0,0 +1,623 @@
|
||||
{% from "_emailhelpers.html" import render_text, text, render_button, raw_url, grey_section, section %}
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin: .4em 0 1.1875em;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* Utilities ------------------------------ */
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
/* Buttons ------------------------------ */
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
border-right: 18px solid #3869D4;
|
||||
border-bottom: 10px solid #3869D4;
|
||||
border-left: 18px solid #3869D4;
|
||||
display: inline-block;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
border-right: 18px solid #22BC66;
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
border-right: 18px solid #FF6136;
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
/* Attribute list ------------------------------ */
|
||||
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
/* Related Items ------------------------------ */
|
||||
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
/* Discount Code ------------------------------ */
|
||||
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
/* Social Icons ------------------------------ */
|
||||
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
/* Data table ------------------------------ */
|
||||
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 35px 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
/* Masthead ----------------------- */
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #A8AAAF;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
/* Body ------------------------------ */
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.email-body_inner {
|
||||
width: 750px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 750px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
width: 750px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 750px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
padding: 30px;
|
||||
}
|
||||
/*Media Queries ------------------------------ */
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body,
|
||||
.email-body,
|
||||
.email-body_inner,
|
||||
.email-content,
|
||||
.email-wrapper,
|
||||
.email-masthead,
|
||||
.email-footer {
|
||||
background-color: #333333 !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
color: #FFF !important;
|
||||
}
|
||||
.attributes_content,
|
||||
.discount {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
.email-masthead_name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="width: 100% !important;
|
||||
height: 100%;
|
||||
-webkit-text-size-adjust: none;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
margin: 0;"
|
||||
bgcolor="#F2F4F6">
|
||||
<span class="preheader"
|
||||
style="display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;">{{ pre_header }}</span>
|
||||
<table class="email-wrapper"
|
||||
width="100%"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 100%;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
margin: 0;
|
||||
padding: 0;"
|
||||
bgcolor="#F2F4F6">
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;">
|
||||
<table class="email-content"
|
||||
width="100%"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 100%;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
margin: 0;
|
||||
padding: 0;">
|
||||
<tr>
|
||||
<td class="email-masthead"
|
||||
style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
padding: 25px 0;"
|
||||
align="center">
|
||||
<a href="{{ LANDING_PAGE_URL }}"
|
||||
class="f-fallback email-masthead_name"
|
||||
style="color: #A8AAAF;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;">
|
||||
{% block logo %}<img src="{{ URL }}/static/logo.png" style="width: 150px; margin: auto">{% endblock %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Email Body -->
|
||||
<tr>
|
||||
<td class="email-body"
|
||||
width="750"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="word-break: break-word;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;">
|
||||
<table class="email-body_inner"
|
||||
align="center"
|
||||
width="750"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 750px;
|
||||
-premailer-width: 750px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
margin: 0 auto;
|
||||
padding: 0;"
|
||||
bgcolor="#FFFFFF">
|
||||
<!-- Body content -->
|
||||
<tr>
|
||||
<td class="content-cell"
|
||||
style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
padding: 30px;">
|
||||
<div class="f-fallback">
|
||||
{% block greeting %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
<!-- Sub copy -->
|
||||
{% block sub_copy %}{% endblock %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;">
|
||||
<table class="email-footer"
|
||||
align="center"
|
||||
width="750"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 750px;
|
||||
-premailer-width: 750px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
padding: 0;">
|
||||
<tr>
|
||||
<td class="content-cell"
|
||||
align="center"
|
||||
style="word-break: break-word;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
padding: 30px;">
|
||||
<p class="f-fallback sub align-center"
|
||||
style="font-size: 13px;
|
||||
line-height: 1.625;
|
||||
text-align: center;
|
||||
color: #A8AAAF;
|
||||
margin: .4em 0 1.1875em;"
|
||||
align="center">
|
||||
© {{ YEAR }} SimpleLogin - a Proton product. All rights reserved.
|
||||
<br />
|
||||
{% block footer %}{% endblock %}
|
||||
</p>
|
||||
{% if unsubscribe_oneclick is defined %}
|
||||
|
||||
<p class="f-fallback sub align-center"
|
||||
style="font-size: 13px;
|
||||
line-height: 1.625;
|
||||
text-align: center;
|
||||
margin: .4em 0 1.1875em;">
|
||||
<a href="{{ unsubscribe_oneclick }}">Unsubscribe from our newsletter</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="f-fallback sub align-center"
|
||||
style="font-size: 13px;
|
||||
line-height: 1.625;
|
||||
text-align: center;
|
||||
color: #A8AAAF;
|
||||
margin: .4em 0 1.1875em;"
|
||||
align="center">
|
||||
<a href="https://app.simplelogin.io/dashboard/support">Do you have a question?</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
@ -6,6 +6,7 @@
|
||||
{{ render_text("Your subscription will end on " + next_bill_date + ".") }}
|
||||
{{ render_text("When the subscription ends:") }}
|
||||
{{ render_text("- All aliases/domains/directories you have created are <b>kept</b> and continue working normally.") }}
|
||||
{{ render_text("- You cannot create new reverse aliases.") }}
|
||||
{% call text() %}
|
||||
- You cannot create new aliases if you exceed the free plan limit, i.e. have more than {{ MAX_NB_EMAIL_FREE_PLAN }} aliases.
|
||||
{% endcall %}
|
||||
|
@ -9,6 +9,7 @@ When the subscription ends:
|
||||
|
||||
- All aliases/domains/directories you have created are kept and continue working.
|
||||
- You cannot create new aliases if you exceed the free plan limit, i.e. have more than {{MAX_NB_EMAIL_FREE_PLAN}} aliases.
|
||||
- You cannot create new reverse aliases.
|
||||
- As features like "catch-all" or "directory" allow you to create aliases on-the-fly,
|
||||
those aliases cannot be automatically created if you have more than {{MAX_NB_EMAIL_FREE_PLAN}} aliases.
|
||||
- You cannot add new domain or directory.
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
{{ render_text("- You cannot add new domain or directory.") }}
|
||||
{{ render_text("- You cannot add new mailbox.") }}
|
||||
{{ render_text("- You cannot create new reverse aliases.") }}
|
||||
{{ render_text("- If you enable PGP Encryption, forwarded emails are not encrypted anymore.") }}
|
||||
{{ render_text('You can upgrade today to continue using all these Premium features (and much more coming).') }}
|
||||
{{ render_button("Upgrade your account", URL ~ "/dashboard/pricing") }}
|
||||
|
@ -8,6 +8,7 @@ When the trial ends:
|
||||
- All aliases/domains/directories you have created are kept and continue working.
|
||||
- You cannot create new aliases if you exceed the free plan limit, i.e. have more than {{MAX_NB_EMAIL_FREE_PLAN}} aliases.
|
||||
- You cannot add new domain or directory.
|
||||
- You cannot create new reverse aliases.
|
||||
- You cannot add new mailbox.
|
||||
- If you enable PGP Encryption, forwarded emails are not encrypted anymore.
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
from app.db import Session
|
||||
from app.models import Alias, Mailbox, AliasMailbox
|
||||
from app.models import Alias, Mailbox, AliasMailbox, User
|
||||
from tests.utils import create_new_user, random_email
|
||||
|
||||
|
||||
@ -15,3 +15,17 @@ def test_duplicated_mailbox_is_returned_only_once():
|
||||
alias_mailbox_id = [mailbox.id for mailbox in alias_mailboxes]
|
||||
assert user.default_mailbox_id in alias_mailbox_id
|
||||
assert other_mailbox.id in alias_mailbox_id
|
||||
|
||||
|
||||
def test_alias_create_from_partner_flags_also_the_user():
|
||||
user = create_new_user()
|
||||
Session.flush()
|
||||
email = random_email()
|
||||
alias = Alias.create(
|
||||
user_id=user.id,
|
||||
email=email,
|
||||
mailbox_id=user.default_mailbox_id,
|
||||
flags=Alias.FLAG_PARTNER_CREATED,
|
||||
flush=True,
|
||||
)
|
||||
assert alias.user.flags & User.FLAG_CREATED_ALIAS_FROM_PARTNER > 0
|
||||
|
@ -9,6 +9,7 @@ import pytest
|
||||
from app import config
|
||||
from app.config import MAX_ALERT_24H, ROOT_DIR
|
||||
from app.db import Session
|
||||
from app.email import headers
|
||||
from app.email_utils import (
|
||||
get_email_domain_part,
|
||||
can_create_directory_for_address,
|
||||
@ -354,6 +355,33 @@ def test_is_valid_email():
|
||||
assert not is_valid_email("emoji👌@gmail.com")
|
||||
|
||||
|
||||
def test_add_subject_prefix():
|
||||
msg = email.message_from_string(
|
||||
"""Subject: Potato
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
hello
|
||||
"""
|
||||
)
|
||||
new_msg = add_header(msg, "text header", "html header", subject_prefix="[TEST]")
|
||||
assert "text header" in new_msg.as_string()
|
||||
assert "html header" not in new_msg.as_string()
|
||||
assert new_msg[headers.SUBJECT] == "[TEST] Potato"
|
||||
|
||||
|
||||
def test_add_subject_prefix_with_no_header():
|
||||
msg = email.message_from_string(
|
||||
"""Content-Transfer-Encoding: 7bit
|
||||
|
||||
hello
|
||||
"""
|
||||
)
|
||||
new_msg = add_header(msg, "text header", "html header", subject_prefix="[TEST]")
|
||||
assert "text header" in new_msg.as_string()
|
||||
assert "html header" not in new_msg.as_string()
|
||||
assert new_msg[headers.SUBJECT] == "[TEST]"
|
||||
|
||||
|
||||
def test_add_header_plain_text():
|
||||
msg = email.message_from_string(
|
||||
"""Content-Type: text/plain; charset=us-ascii
|
||||
|
Loading…
x
Reference in New Issue
Block a user