diff --git a/app/admin_model.py b/app/admin_model.py index d9962351..9d05e4fb 100644 --- a/app/admin_model.py +++ b/app/admin_model.py @@ -5,7 +5,7 @@ from flask import redirect, url_for, request, flash from flask_admin import expose, AdminIndexView from flask_admin.actions import action from flask_admin.contrib import sqla -from flask_login import current_user, login_user +from flask_login import current_user from app.log import LOG from app.db import Session @@ -91,7 +91,7 @@ class UserAdmin(SLModelView): "fido_uuid", "profile_picture", ] - can_edit = True + can_edit = False def scaffold_list_columns(self): ret = super().scaffold_list_columns() @@ -138,6 +138,14 @@ class UserAdmin(SLModelView): def action_monero_upgrade(self, ids): manual_upgrade("Crypto", ids, is_giveaway=False) + @action( + "adhoc_upgrade", + "Adhoc upgrade - for exceptional case", + "Are you sure you want to crypto-upgrade selected users?", + ) + def action_adhoc_upgrade(self, ids): + manual_upgrade("Adhoc", ids, is_giveaway=False) + @action( "extend_trial_1w", "Extend trial for 1 week more", @@ -178,22 +186,21 @@ class UserAdmin(SLModelView): Session.commit() - @action( - "login_as", - "Login as this user", - "Login as this user?", - ) - def login_as(self, ids): - if len(ids) != 1: - flash("only 1 user can be selected", "error") - return - - for user in User.filter(User.id.in_(ids)): - AdminAuditLog.logged_as_user(current_user.id, user.id) - Session.commit() - login_user(user) - flash(f"Login as user {user}", "success") - return redirect("/") + # @action( + # "login_as", + # "Login as this user", + # "Login as this user?", + # ) + # def login_as(self, ids): + # if len(ids) != 1: + # flash("only 1 user can be selected", "error") + # return + # + # for user in User.filter(User.id.in_(ids)): + # AdminAuditLog.logged_as_user(current_user.id, user.id) + # login_user(user) + # flash(f"Login as user {user}", "success") + # return redirect("/") def manual_upgrade(way: str, ids: [int], is_giveaway: bool): @@ -258,18 +265,18 @@ class MailboxAdmin(SLModelView): column_filters = ["id", "user.email", "email"] -class LifetimeCouponAdmin(SLModelView): - can_edit = True - can_create = True +# class LifetimeCouponAdmin(SLModelView): +# can_edit = True +# can_create = True class CouponAdmin(SLModelView): - can_edit = True + can_edit = False can_create = True class ManualSubscriptionAdmin(SLModelView): - can_edit = True + can_edit = False column_searchable_list = ["id", "user.email"] @action( @@ -280,15 +287,27 @@ class ManualSubscriptionAdmin(SLModelView): def extend_1y(self, ids): for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)): ms.end_at = ms.end_at.shift(years=1) - flash(f"Extend subscription for {ms.user}", "success") + flash(f"Extend subscription for 1 year for {ms.user}", "success") + + Session.commit() + + @action( + "extend_1m", + "Extend for 1 month", + "Extend 1 month more?", + ) + def extend_1m(self, ids): + for ms in ManualSubscription.filter(ManualSubscription.id.in_(ids)): + ms.end_at = ms.end_at.shift(months=1) + flash(f"Extend subscription for 1 month for {ms.user}", "success") Session.commit() -class ClientAdmin(SLModelView): - column_searchable_list = ["name", "description", "user.email"] - column_exclude_list = ["oauth_client_secret", "home_url"] - can_edit = True +# class ClientAdmin(SLModelView): +# column_searchable_list = ["name", "description", "user.email"] +# column_exclude_list = ["oauth_client_secret", "home_url"] +# can_edit = True class CustomDomainAdmin(SLModelView): @@ -308,12 +327,12 @@ class ReferralAdmin(SLModelView): return ret -class PayoutAdmin(SLModelView): - column_searchable_list = ["id", "user.email"] - column_filters = ["id", "user.email"] - can_edit = True - can_create = True - can_delete = True +#class PayoutAdmin(SLModelView): +# column_searchable_list = ["id", "user.email"] +# column_filters = ["id", "user.email"] +# can_edit = True +# can_create = True +# can_delete = True class AdminAuditLogAdmin(SLModelView): diff --git a/app/api/serializer.py b/app/api/serializer.py index 2d9546b6..21b801de 100644 --- a/app/api/serializer.py +++ b/app/api/serializer.py @@ -204,7 +204,7 @@ def get_alias_infos_with_pagination_v3( q = list(q.limit(page_limit).offset(page_id * page_size)) ret = [] - for alias, contact, email_log, custom_domain, nb_reply, nb_blocked, nb_forward in q: + for alias, contact, email_log, nb_reply, nb_blocked, nb_forward in q: ret.append( AliasInfo( alias=alias, @@ -215,7 +215,7 @@ def get_alias_infos_with_pagination_v3( nb_reply=nb_reply, latest_email_log=email_log, latest_contact=contact, - custom_domain=custom_domain, + custom_domain=alias.custom_domain, ) ) @@ -318,7 +318,7 @@ def get_alias_info_v3(user: User, alias_id: int) -> AliasInfo: q = construct_alias_query(user) q = q.filter(Alias.id == alias_id) - for alias, contact, email_log, custom_domain, nb_reply, nb_blocked, nb_forward in q: + for alias, contact, email_log, nb_reply, nb_blocked, nb_forward in q: return AliasInfo( alias=alias, mailbox=alias.mailbox, @@ -328,7 +328,7 @@ def get_alias_info_v3(user: User, alias_id: int) -> AliasInfo: nb_reply=nb_reply, latest_email_log=email_log, latest_contact=contact, - custom_domain=custom_domain, + custom_domain=alias.custom_domain, ) @@ -379,14 +379,13 @@ def construct_alias_query(user: User): Alias, Contact, EmailLog, - CustomDomain, alias_activity_subquery.c.nb_reply, alias_activity_subquery.c.nb_blocked, alias_activity_subquery.c.nb_forward, ) .options(joinedload(Alias.hibp_breaches)) + .options(joinedload(Alias.custom_domain)) .join(Contact, Alias.id == Contact.alias_id, isouter=True) - .join(CustomDomain, Alias.custom_domain_id == CustomDomain.id, isouter=True) .join(EmailLog, Contact.id == EmailLog.contact_id, isouter=True) .filter(Alias.id == alias_activity_subquery.c.id) .filter(Alias.id == alias_contact_subquery.c.id) diff --git a/app/config.py b/app/config.py index 2148eb42..97605cdc 100644 --- a/app/config.py +++ b/app/config.py @@ -431,3 +431,11 @@ def get_allowed_redirect_domains() -> List[str]: ALLOWED_REDIRECT_DOMAINS = get_allowed_redirect_domains() + + +def setup_nameservers(): + nameservers = os.environ.get("NAMESERVERS", "1.1.1.1") + return nameservers.split(",") + + +NAMESERVERS = setup_nameservers() diff --git a/app/dashboard/views/notification.py b/app/dashboard/views/notification.py index 82270e4f..e22c8e2c 100644 --- a/app/dashboard/views/notification.py +++ b/app/dashboard/views/notification.py @@ -23,6 +23,10 @@ def notification_route(notification_id): ) return redirect(url_for("dashboard.index")) + if not notification.read: + notification.read = True + Session.commit() + if request.method == "POST": notification_title = notification.title or notification.message[:20] Notification.delete(notification_id) diff --git a/app/dashboard/views/setting.py b/app/dashboard/views/setting.py index 50699628..b9af1e0b 100644 --- a/app/dashboard/views/setting.py +++ b/app/dashboard/views/setting.py @@ -315,6 +315,15 @@ def setting(): return redirect(url_for("dashboard.setting")) Session.commit() flash("Your preference has been updated", "success") + elif request.form.get("form-name") == "sender-header": + choose = request.form.get("enable") + if choose == "on": + current_user.include_header_email_header = True + else: + current_user.include_header_email_header = False + Session.commit() + flash("Your preference has been updated", "success") + return redirect(url_for("dashboard.setting")) elif request.form.get("form-name") == "export-data": return redirect(url_for("api.export_data")) elif request.form.get("form-name") == "export-alias": diff --git a/app/discover/views/index.py b/app/discover/views/index.py index a8017c61..2ed84704 100644 --- a/app/discover/views/index.py +++ b/app/discover/views/index.py @@ -8,5 +8,5 @@ from app.models import Client @discover_bp.route("/", methods=["GET", "POST"]) @login_required def index(): - clients = Client.filter_by(published=True).all() + clients = Client.filter_by(approved=True).all() return render_template("discover/index.html", clients=clients) diff --git a/app/dns_utils.py b/app/dns_utils.py index 0c27b939..5fd5c12b 100644 --- a/app/dns_utils.py +++ b/app/dns_utils.py @@ -1,3 +1,4 @@ +from app import config from typing import Optional, List, Tuple import dns.resolver @@ -5,16 +6,14 @@ import dns.resolver def _get_dns_resolver(): my_resolver = dns.resolver.Resolver() - - # 1.1.1.1 is CloudFlare's public DNS server - my_resolver.nameservers = ["1.1.1.1"] + my_resolver.nameservers = config.NAMESERVERS return my_resolver def get_ns(hostname) -> [str]: try: - answers = _get_dns_resolver().resolve(hostname, "NS") + answers = _get_dns_resolver().resolve(hostname, "NS", search=True) except Exception: return [] return [a.to_text() for a in answers] @@ -23,7 +22,7 @@ def get_ns(hostname) -> [str]: def get_cname_record(hostname) -> Optional[str]: """Return the CNAME record if exists for a domain, WITHOUT the trailing period at the end""" try: - answers = _get_dns_resolver().resolve(hostname, "CNAME") + answers = _get_dns_resolver().resolve(hostname, "CNAME", search=True) except Exception: return None @@ -39,7 +38,7 @@ def get_mx_domains(hostname) -> [(int, str)]: domain name ends with a "." at the end. """ try: - answers = _get_dns_resolver().resolve(hostname, "MX") + answers = _get_dns_resolver().resolve(hostname, "MX", search=True) except Exception: return [] @@ -60,7 +59,7 @@ _include_spf = "include:" def get_spf_domain(hostname) -> [str]: """return all domains listed in *include:*""" try: - answers = _get_dns_resolver().resolve(hostname, "TXT") + answers = _get_dns_resolver().resolve(hostname, "TXT", search=True) except Exception: return [] @@ -82,7 +81,7 @@ def get_spf_domain(hostname) -> [str]: def get_txt_record(hostname) -> [str]: """return all domains listed in *include:*""" try: - answers = _get_dns_resolver().resolve(hostname, "TXT") + answers = _get_dns_resolver().resolve(hostname, "TXT", search=True) except Exception: return [] @@ -112,10 +111,10 @@ def is_mx_equivalent( ref_mx_domains, key=lambda priority_domain: priority_domain[0] ) - if len(mx_domains) != len(ref_mx_domains): + if len(mx_domains) < len(ref_mx_domains): return False - for i in range(0, len(mx_domains)): + for i in range(0, len(ref_mx_domains)): if mx_domains[i][1] != ref_mx_domains[i][1]: return False diff --git a/app/email/headers.py b/app/email/headers.py index 3b5be9e8..e1aaabd6 100644 --- a/app/email/headers.py +++ b/app/email/headers.py @@ -2,7 +2,7 @@ MESSAGE_ID = "Message-ID" IN_REPLY_TO = "In-Reply-To" REFERENCES = "References" -DATE = "date" +DATE = "Date" SUBJECT = "Subject" FROM = "From" TO = "To" diff --git a/app/email_utils.py b/app/email_utils.py index 69957e40..23ca2c2f 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -1118,15 +1118,17 @@ def normalize_reply_email(reply_email: str) -> str: return "".join(ret) -def should_disable(alias: Alias) -> bool: - """Disable an alias if it has too many bounces recently""" +def should_disable(alias: Alias) -> (bool, str): + """ + Return whether an alias should be disabled and if yes, the reason why + """ # Bypass the bounce rule if alias.cannot_be_disabled: LOG.w("%s cannot be disabled", alias) - return False + return False, "" if not ALIAS_AUTOMATIC_DISABLE: - return False + return False, "" yesterday = arrow.now().shift(days=-1) nb_bounced_last_24h = ( @@ -1141,12 +1143,11 @@ def should_disable(alias: Alias) -> bool: ) # if more than 12 bounces in 24h -> disable alias if nb_bounced_last_24h > 12: - LOG.d("more than 12 bounces in the last 24h, disable alias %s", alias) - return True + return True, "+12 bounces in the last 24h" - # if more than 5 bounces but has bounces last week -> disable alias + # if more than 5 bounces but has +10 bounces last week -> disable alias elif nb_bounced_last_24h > 5: - one_week_ago = arrow.now().shift(days=-8) + one_week_ago = arrow.now().shift(days=-7) nb_bounced_7d_1d = ( Session.query(EmailLog) .filter( @@ -1158,16 +1159,14 @@ def should_disable(alias: Alias) -> bool: .filter(EmailLog.alias_id == alias.id) .count() ) - if nb_bounced_7d_1d > 1: - LOG.d( - "more than 5 bounces in the last 24h and more than 1 bounces in the last 7 days, " - "disable alias %s", - alias, + if nb_bounced_7d_1d > 10: + return ( + True, + "+5 bounces in the last 24h and +10 bounces in the last 7 days", ) - return True else: # alias level - # if bounces at least 9 days in the last 10 days -> disable alias + # if bounces happen for at least 9 days in the last 10 days -> disable alias query = ( Session.query( func.date(EmailLog.created_at).label("date"), @@ -1183,11 +1182,7 @@ def should_disable(alias: Alias) -> bool: ) if query.count() >= 9: - LOG.d( - "Bounces every day for at least 9 days in the last 10 days, disable alias %s", - alias, - ) - return True + return True, "Bounces every day for at least 9 days in the last 10 days" # account level query = ( @@ -1206,16 +1201,13 @@ def should_disable(alias: Alias) -> bool: # if an account has more than 10 bounces every day for at least 4 days in the last 10 days, disable alias date_bounces: List[Tuple[arrow.Arrow, int]] = list(query) - if len(date_bounces) > 4: - if all([v > 10 for _, v in date_bounces]): - LOG.d( - "+10 bounces for +4 days in the last 10 days on %s, disable alias %s", - alias.user, - alias, - ) - return True + more_than_10_bounces = [ + (d, nb_bounce) for d, nb_bounce in date_bounces if nb_bounce > 10 + ] + if len(more_than_10_bounces) > 4: + return True, "+10 bounces for +4 days in the last 10 days" - return False + return False, "" def parse_id_from_bounce(email_address: str) -> int: diff --git a/app/models.py b/app/models.py index f78b301b..5cc12795 100644 --- a/app/models.py +++ b/app/models.py @@ -450,6 +450,11 @@ class User(Base, ModelMixin, UserMixin, PasswordOracle): server_default=BlockBehaviourEnum.return_2xx.name, ) + # to keep existing behavior, the server default is TRUE whereas for new user, the default value is FALSE + include_header_email_header = sa.Column( + sa.Boolean, default=False, nullable=False, server_default="1" + ) + @property def directory_quota(self): return min( @@ -1160,7 +1165,7 @@ class Alias(Base, ModelMixin): enabled = sa.Column(sa.Boolean(), default=True, nullable=False) custom_domain_id = sa.Column( - sa.ForeignKey("custom_domain.id", ondelete="cascade"), nullable=True + sa.ForeignKey("custom_domain.id", ondelete="cascade"), nullable=True, index=True ) custom_domain = orm.relationship("CustomDomain", foreign_keys=[custom_domain_id]) @@ -1172,7 +1177,7 @@ class Alias(Base, ModelMixin): # to know whether an alias belongs to a directory directory_id = sa.Column( - sa.ForeignKey("directory.id", ondelete="cascade"), nullable=True + sa.ForeignKey("directory.id", ondelete="cascade"), nullable=True, index=True ) note = sa.Column(sa.Text, default=None, nullable=True) diff --git a/cron.py b/cron.py index ced9a6b9..0c64eac4 100644 --- a/cron.py +++ b/cron.py @@ -465,7 +465,7 @@ def alias_creation_report() -> List[Tuple[str, int]]: return res -def growth_stats(): +def stats(): """send admin stats everyday""" if not ADMIN_EMAIL: LOG.w("ADMIN_EMAIL not set, nothing to do") @@ -480,7 +480,7 @@ def growth_stats(): today = arrow.now().format() - report = f""" + growth_stats = f""" Growth Stats for {today} nb_user: {stats_today.nb_user} - {increase_percent(stats_yesterday.nb_user, stats_today.nb_user)} @@ -507,32 +507,16 @@ nb_referred_user: {stats_today.nb_referred_user} - {increase_percent(stats_yeste nb_referred_user_upgrade: {stats_today.nb_referred_user_paid} - {increase_percent(stats_yesterday.nb_referred_user_paid, stats_today.nb_referred_user_paid)} """ - LOG.d("report email: %s", report) + LOG.d("growth_stats email: %s", growth_stats) send_email( ADMIN_EMAIL, subject=f"SimpleLogin Growth Stats for {today}", - plaintext=report, + plaintext=growth_stats, retries=3, ) - -def daily_monitoring_report(): - """send monitoring stats of the previous day""" - if not MONITORING_EMAIL: - LOG.w("MONITORING_EMAIL not set, nothing to do") - return - - stats_today = compute_metric2() - stats_yesterday = ( - Metric2.filter(Metric2.date < stats_today.date) - .order_by(Metric2.date.desc()) - .first() - ) - - today = arrow.now().format() - - report = f""" + monitoring_report = f""" Monitoring Stats for {today} nb_alias: {stats_today.nb_alias} - {increase_percent(stats_yesterday.nb_alias, stats_today.nb_alias)} @@ -545,32 +529,32 @@ nb_total_bounced_last_24h: {stats_today.nb_total_bounced_last_24h} - {increase_p """ - report += "\n====================================\n" - report += f""" + monitoring_report += "\n====================================\n" + monitoring_report += f""" # Account bounce report: """ for email, bounces in bounce_report(): - report += f"{email}: {bounces}\n" + monitoring_report += f"{email}: {bounces}\n" - report += f"""\n + monitoring_report += f"""\n # Alias creation report: """ for email, nb_alias, date in alias_creation_report(): - report += f"{email}, {date}: {nb_alias}\n" + monitoring_report += f"{email}, {date}: {nb_alias}\n" - report += f"""\n + monitoring_report += f"""\n # Full bounce detail report: """ - report += all_bounce_report() + monitoring_report += all_bounce_report() - LOG.d("report email: %s", report) + LOG.d("monitoring_report email: %s", monitoring_report) send_email( MONITORING_EMAIL, subject=f"SimpleLogin Monitoring Report for {today}", - plaintext=report, + plaintext=monitoring_report, retries=3, ) @@ -1040,8 +1024,7 @@ if __name__ == "__main__": help="Choose a cron job to run", type=str, choices=[ - "growth_stats", - "daily_monitoring_report", + "stats", "notify_trial_end", "notify_manual_subscription_end", "notify_premium_end", @@ -1057,12 +1040,9 @@ if __name__ == "__main__": args = parser.parse_args() # wrap in an app context to benefit from app setup like database cleanup, sentry integration, etc with create_light_app().app_context(): - if args.job == "growth_stats": - LOG.d("Compute growth Stats") - growth_stats() - if args.job == "daily_monitoring_report": - LOG.d("Send out daily monitoring stats") - daily_monitoring_report() + if args.job == "stats": + LOG.d("Compute growth and daily monitoring stats") + stats() elif args.job == "notify_trial_end": LOG.d("Notify users with trial ending soon") notify_trial_end() diff --git a/crontab.yml b/crontab.yml index adf463cb..f52dd908 100644 --- a/crontab.yml +++ b/crontab.yml @@ -1,12 +1,6 @@ jobs: - name: SimpleLogin growth stats - command: python /code/cron.py -j growth_stats - shell: /bin/bash - schedule: "0 1 * * *" - captureStderr: true - - - name: SimpleLogin monitoring stats - command: python /code/cron.py -j daily_monitoring_report + command: python /code/cron.py -j stats shell: /bin/bash schedule: "0 0 * * *" captureStderr: true diff --git a/email_handler.py b/email_handler.py index 3f36eabf..67ba6a12 100644 --- a/email_handler.py +++ b/email_handler.py @@ -801,7 +801,8 @@ def forward_email_to_mailbox( add_or_replace_header(msg, headers.SL_DIRECTION, "Forward") msg[headers.SL_EMAIL_LOG_ID] = str(email_log.id) - msg[headers.SL_ENVELOPE_FROM] = envelope.mail_from + if user.include_header_email_header: + msg[headers.SL_ENVELOPE_FROM] = envelope.mail_from # when an alias isn't in the To: header, there's no way for users to know what alias has received the email msg[headers.SL_ENVELOPE_TO] = alias.email @@ -1394,8 +1395,44 @@ def handle_bounce_forward_phase(msg: Message, email_log: EmailLog): refused_email_url = f"{URL}/dashboard/refused_email?highlight_id={email_log.id}" - # inform user of this bounce - if not should_disable(alias): + alias_will_be_disabled, reason = should_disable(alias) + if alias_will_be_disabled: + LOG.w( + f"Disable alias {alias} because {reason}. {alias.mailboxes} {alias.user}. Last contact {contact}" + ) + alias.enabled = False + + Notification.create( + user_id=user.id, + title=f"{alias.email} has been disabled due to multiple bounces", + message=Notification.render( + "notification/alias-disable.html", alias=alias, mailbox=mailbox + ), + ) + + Session.commit() + + send_email_with_rate_control( + user, + ALERT_BOUNCE_EMAIL, + user.email, + f"Alias {alias.email} has been disabled due to multiple bounces", + render( + "transactional/bounce/automatic-disable-alias.txt", + alias=alias, + refused_email_url=refused_email_url, + mailbox_email=mailbox.email, + ), + render( + "transactional/bounce/automatic-disable-alias.html", + alias=alias, + refused_email_url=refused_email_url, + mailbox_email=mailbox.email, + ), + max_nb_alert=10, + ignore_smtp_error=True, + ) + else: LOG.d( "Inform user %s about a bounce from contact %s to alias %s", user, @@ -1445,31 +1482,6 @@ def handle_bounce_forward_phase(msg: Message, email_log: EmailLog): # smtp error can happen if user mailbox is unreachable, that might explain the bounce ignore_smtp_error=True, ) - else: - LOG.w("Disable alias %s %s. Last contact %s", alias, user, contact) - alias.enabled = False - Session.commit() - - send_email_with_rate_control( - user, - ALERT_BOUNCE_EMAIL, - user.email, - f"Alias {alias.email} has been disabled due to multiple bounces", - render( - "transactional/bounce/automatic-disable-alias.txt", - alias=alias, - refused_email_url=refused_email_url, - mailbox_email=mailbox.email, - ), - render( - "transactional/bounce/automatic-disable-alias.html", - alias=alias, - refused_email_url=refused_email_url, - mailbox_email=mailbox.email, - ), - max_nb_alert=10, - ignore_smtp_error=True, - ) def handle_hotmail_complaint(msg: Message) -> bool: @@ -1899,18 +1911,23 @@ def handle_unsubscribe(envelope: Envelope, msg: Message) -> str: alias_id = int(subject) alias = Alias.get(alias_id) except Exception: - LOG.w("Cannot parse alias from subject %s", msg[headers.SUBJECT]) + LOG.w("Wrong format subject %s", msg[headers.SUBJECT]) return status.E507 if not alias: - LOG.w("No such alias %s", alias_id) + LOG.w("Cannot get alias from subject %s", subject) return status.E508 mail_from = envelope.mail_from # Only alias's owning mailbox can send the unsubscribe request mailbox = get_mailbox_from_mail_from(mail_from, alias) if not mailbox: - LOG.d("%s cannot disable alias %s", envelope.mail_from, alias) + LOG.d( + "%s cannot disable alias %s. Alias authorized addresses:%s", + envelope.mail_from, + alias, + alias.authorized_addresses, + ) return status.E509 user = alias.user diff --git a/example.env b/example.env index e3b9cadb..31429f75 100644 --- a/example.env +++ b/example.env @@ -174,4 +174,8 @@ DISABLE_ONBOARDING=true #ALIAS_AUTOMATIC_DISABLE=true # domains that can be present in the &next= section when using absolute urls -ALLOWED_REDIRECT_DOMAINS=[] \ No newline at end of file +ALLOWED_REDIRECT_DOMAINS=[] + +# DNS nameservers to be used by the app +# Multiple nameservers can be specified, separated by ',' +NAMESERVERS="1.1.1.1" diff --git a/migrations/versions/2022_022512_4729b7096d12_.py b/migrations/versions/2022_022512_4729b7096d12_.py new file mode 100644 index 00000000..66c8c721 --- /dev/null +++ b/migrations/versions/2022_022512_4729b7096d12_.py @@ -0,0 +1,29 @@ +"""empty message + +Revision ID: 4729b7096d12 +Revises: 9282e982bc05 +Create Date: 2022-02-25 12:11:10.991810 + +""" +import sqlalchemy_utils +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4729b7096d12' +down_revision = '5047fcbd57c7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('include_header_email_header', sa.Boolean(), server_default='1', nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'include_header_email_header') + # ### end Alembic commands ### diff --git a/migrations/versions/2022_022617_5047fcbd57c7_.py b/migrations/versions/2022_022617_5047fcbd57c7_.py new file mode 100644 index 00000000..0485d29d --- /dev/null +++ b/migrations/versions/2022_022617_5047fcbd57c7_.py @@ -0,0 +1,31 @@ +"""empty message + +Revision ID: 5047fcbd57c7 +Revises: 9282e982bc05 +Create Date: 2022-02-26 17:51:03.379676 + +""" +import sqlalchemy_utils +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5047fcbd57c7' +down_revision = '9282e982bc05' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index(op.f('ix_alias_custom_domain_id'), 'alias', ['custom_domain_id'], unique=False) + op.create_index(op.f('ix_alias_directory_id'), 'alias', ['directory_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_alias_directory_id'), table_name='alias') + op.drop_index(op.f('ix_alias_custom_domain_id'), table_name='alias') + # ### end Alembic commands ### diff --git a/monitoring.py b/monitoring.py index b10c6122..cf4fb87a 100644 --- a/monitoring.py +++ b/monitoring.py @@ -50,6 +50,7 @@ if __name__ == "__main__": while True: log_postfix_metrics() log_nb_db_connection() + Session.close() # 1 min sleep(60) diff --git a/server.py b/server.py index 775427ec..d05308c1 100644 --- a/server.py +++ b/server.py @@ -33,11 +33,7 @@ from app.admin_model import ( EmailLogAdmin, AliasAdmin, MailboxAdmin, - LifetimeCouponAdmin, ManualSubscriptionAdmin, - ClientAdmin, - ReferralAdmin, - PayoutAdmin, CouponAdmin, CustomDomainAdmin, AdminAuditLogAdmin, @@ -82,20 +78,16 @@ from app.fake_data import fake_data from app.jose_utils import get_jwk_key from app.log import LOG from app.models import ( - Client, User, Alias, Subscription, PlanEnum, CustomDomain, - LifetimeCoupon, Mailbox, - Referral, CoinbaseSubscription, EmailLog, Contact, ManualSubscription, - Payout, Coupon, AdminAuditLog, ) @@ -695,13 +687,9 @@ def init_admin(app): admin.add_view(AliasAdmin(Alias, Session)) admin.add_view(MailboxAdmin(Mailbox, Session)) admin.add_view(EmailLogAdmin(EmailLog, Session)) - admin.add_view(LifetimeCouponAdmin(LifetimeCoupon, Session)) admin.add_view(CouponAdmin(Coupon, Session)) admin.add_view(ManualSubscriptionAdmin(ManualSubscription, Session)) - admin.add_view(ClientAdmin(Client, Session)) admin.add_view(CustomDomainAdmin(CustomDomain, Session)) - admin.add_view(ReferralAdmin(Referral, Session)) - admin.add_view(PayoutAdmin(Payout, Session)) admin.add_view(AdminAuditLogAdmin(AdminAuditLog, Session)) diff --git a/static/js/utils/drag-drop-into-text.js b/static/js/utils/drag-drop-into-text.js new file mode 100644 index 00000000..ab4b062f --- /dev/null +++ b/static/js/utils/drag-drop-into-text.js @@ -0,0 +1,28 @@ +const MAX_BYTES = 10240; // 10KiB + +function enableDragDropForPGPKeys(inputID) { + function drop(event) { + event.stopPropagation(); + event.preventDefault(); + + let files = event.dataTransfer.files; + for (let i = 0; i < files.length; i++) { + let file = files[i]; + if(file.type !== 'text/plain'){ + toastr.warning(`File ${file.name} is not a public key file`); + continue; + } + let reader = new FileReader(); + reader.onloadend = onFileLoaded; + reader.readAsBinaryString(file); + } + } + + function onFileLoaded(event) { + const initialData = event.currentTarget.result.substr(0, MAX_BYTES); + $(inputID).val(initialData); + } + + const dropArea = $(inputID).get(0); + dropArea.addEventListener("drop", drop, false); +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 0767fd5e..364c2fa2 100644 --- a/templates/base.html +++ b/templates/base.html @@ -87,7 +87,7 @@ {% if PLAUSIBLE_HOST and PLAUSIBLE_DOMAIN %} - + {% endif %} diff --git a/templates/dashboard/contact_detail.html b/templates/dashboard/contact_detail.html index 25a79776..df09090e 100644 --- a/templates/dashboard/contact_detail.html +++ b/templates/dashboard/contact_detail.html @@ -44,13 +44,16 @@ {% endif %} +
You can drag and drop the pgp key file into the text area
+ +
+ + + +
Alias Import
diff --git a/templates/emails/transactional/cycle-email.html b/templates/emails/transactional/cycle-email.html index 7c705cf8..cc68a678 100644 --- a/templates/emails/transactional/cycle-email.html +++ b/templates/emails/transactional/cycle-email.html @@ -19,9 +19,5 @@ The email is automatically deleted in 7 days. {% endcall %} - {% call text() %} - If you have any question, please reply to this email. - {% endcall %} - {{ render_text('Thanks,
SimpleLogin Team.') }} {% endblock %} diff --git a/templates/header.html b/templates/header.html index 3e602142..656317ba 100644 --- a/templates/header.html +++ b/templates/header.html @@ -38,15 +38,13 @@