This commit is contained in:
2022-12-30 16:23:27 +00:00
parent 02776e8478
commit 20da343c54
1304 changed files with 870224 additions and 0 deletions

View File

@ -0,0 +1,286 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Alias Contact Manager{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">
{{ alias.email }} contacts
<a class="ml-3 text-info"
style="font-size: 12px"
data-toggle="collapse"
href="#howtouse"
role="button"
aria-expanded="false"
aria-controls="collapseExample">
How to use <i class="fe fe-chevrons-down"></i>
</a>
</h1>
<div class="alert alert-primary collapse" id="howtouse" role="alert">
<p>
To send an email from your alias to a contact, you need to create a <em>reverse-alias</em>,
a special email address.
<br />
When you send an email to the reverse-alias, the email will be sent <b>from your alias</b> to the contact.
<br />
<img src="/static/images/reverse-alias.svg"
style="border: 1px solid"
class="my-2 img-fluid"/>
</p>
<p>This might seem like "magic" but trust us, only the first time is a bit awkward.</p>
<p>
{% if alias.mailbox_id %}
{% if alias.mailboxes | length == 1 %}
Make sure you send the email from your mailbox <b>{{ alias.mailbox.email }}</b>.
{% else %}
Make sure you send the email from one of the following mailboxes:
<br />
{% for mailbox in alias.mailboxes %}
- <b>{{ mailbox.email }}</b>
<br />
{% endfor %}
{% endif %}
{% else %}
Make sure you send the email from your personal email address ({{ current_user.email }}).
{% endif %}
</p>
<p>
This Youtube video can also quickly walk you through the steps:
<a href="https://www.youtube.com/watch?v=VsypF-DBaow" target="_blank">
How to send emails from an alias <i class="fe fe-external-link"></i>
</a>
</p>
</div>
</div>
</div>
<div class="row mb-5">
<div class="col-12 col-lg-6 pt-1">
<form method="post">
<input type="hidden" name="form-name" value="create" />
{{ new_contact_form.csrf_token }}
{{ new_contact_form.email(class="form-control", placeholder="First Last <email@example.com>", autofocus=True) }}
{{ render_field_errors(new_contact_form.email) }}
<div class="small-text">Where do you want to send the email?</div>
{% if can_create_contacts %}
<button class="btn btn-primary mt-2">Create reverse-alias</button>
{% else %}
<button disabled
title="Upgrade to premium to create reverse-aliases"
class="btn btn-primary mt-2">
Create reverse-alias
</button>
{% endif %}
</form>
</div>
<div class="col-12 col-lg-6 pt-1">
<div class="float-right d-flex">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="search">
<input type="search"
name="query"
value="{{ query }}"
placeholder="Enter to search for contacts"
class="form-control shadow mr-2"
style="max-width: 20em">
</form>
{% if query %}
{% if highlight_contact_id %}
<a href="{{ url_for("dashboard.alias_contact_manager", alias_id=alias.id, highlight_contact_id=highlight_contact_id) }}"
class="btn btn-light">
Reset
</a>
{% else %}
<a href="{{ url_for("dashboard.alias_contact_manager", alias_id=alias.id) }}"
class="btn btn-light">Reset</a>
{% endif %}
{% endif %}
</div>
</div>
</div>
<div class="row">
{% for contact_info in contact_infos %}
{% set contact = contact_info.contact %}
<div class="col-md-6">
<div class="my-2 p-2 card {% if contact.id == highlight_contact_id %} highlight-row{% endif %}">
<div class="mb-2 row">
<div class="col">
<span class="font-weight-bold">{{ contact.website_email }}</span>
{% if contact.pgp_finger_print %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="PGP Enabled">🗝</span>
{% endif %}
</div>
<div class="col text-right">
<label class="custom-switch cursor" data-toggle="tooltip" {% if contact.block_forward %}
title="Unblock sender - start receiving emails from this sender" {% else %} title="Block sender - stop receiving emails from this sender" {% endif %} style="padding-left: 0px">
<input type="checkbox" class="enable-disable-contact custom-switch-input" data-contact="{{ contact.id }}" data-contact-email="{{ contact.website_email }}" {{ "checked" if not contact.block_forward else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</div>
</div>
<div>
<span>
<a href="{{ 'mailto:' + contact.website_send_to() }}"
data-toggle="tooltip"
title="You can click on this to open your email client. Or use the copy button 👉"
class="font-weight-bold">
*************************
</a>
<span class="clipboard btn btn-sm btn-success copy-btn"
data-toggle="tooltip"
title="Copy the reverse-alias to clipboard"
data-clipboard-text="{{ contact.website_send_to() }}">
Copy reverse-alias
</span>
</span>
</div>
<div class="mb-2 text-muted small-text">
{% if contact_info.latest_email_log != None %}
{% set email_log = contact_info.latest_email_log %}
{% if email_log.is_reply %}
<i class="fa fa-reply mr-2"
data-toggle="tooltip"
title="Email reply/sent from alias"></i>
{{ email_log.created_at | dt }}
{% elif email_log.bounced %}
<span class="text-danger">
<i class="fa fa-warning mr-2"
data-toggle="tooltip"
title="Email bounced and cannot be forwarded to your mailbox"></i>
{{ email_log.created_at | dt }}
</span>
{% elif email_log.blocked %}
<i class="fa fa-ban mr-2 text-danger"
data-toggle="tooltip"
title="Email blocked"></i>
{{ email_log.created_at | dt }}
{% else %}
<i class="fa fa-paper-plane mr-2"
data-toggle="tooltip"
title="Email sent to alias"></i>
{{ email_log.created_at | dt }}
{% endif %}
<br />
Contact created {{ contact.created_at | dt }}
{% else %}
No Activity in the last 14 days. Contact created {{ contact.created_at | dt }}
{% endif %}
<div>
<span class="alias-activity">{{ contact_info.nb_forward }}</span> forwarded,
<span class="alias-activity">{{ contact_info.nb_reply }}</span> sent
in the last 14 days.
</div>
</div>
<a href="{{ url_for('dashboard.contact_detail_route', contact_id=contact.id) }}">Edit ➡</a>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete">
<input type="hidden" name="contact-id" value="{{ contact.id }}">
<span class="card-link btn btn-link float-right delete-forward-email text-danger">Delete</span>
</form>
</div>
</div>
{% endfor %}
</div>
{% if nb_contact > PAGE_LIMIT or page > 0 %}
<div class="row mt-3">
<div class="col">
<nav aria-label="Contact navigation">
<ul class="pagination">
<li class="page-item">
<a class="btn btn-outline-secondary {% if page == 0 %}disabled{% endif %}"
href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id, page=page-1) }}">
Previous
</a>
</li>
<li class="page-item">
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}"
href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id, page=page+1) }}">
Next
</a>
</li>
</ul>
</nav>
</div>
</div>
{% endif %}
{% endblock %}
{% block script %}
<script>
$(".delete-forward-email").on("click", function (e) {
let that = $(this);
bootbox.confirm({
message: "All activities associated with this contact will also be deleted, please confirm.",
buttons: {
confirm: {
label: 'Yes, delete it',
className: 'btn-danger'
},
cancel: {
label: 'Cancel',
className: 'btn-outline-primary'
}
},
callback: function (result) {
if (result) {
that.closest("form").submit();
}
}
})
});
$(".enable-disable-contact").change(async function () {
let contactID = $(this).data("contact");
let contactEmail = $(this).data("contact-email");
await blockContact(contactID, contactEmail);
})
async function blockContact(contactID, contactEmail) {
let oldValue;
try {
let res = await fetch(`/api/contacts/${contactID}/toggle`, {
method: "POST",
headers: {
"Content-Type": "application/json",
}
});
if (res.ok) {
let json = await res.json();
if (json.block_forward) {
toastr.success(`${contactEmail} is blocked`);
} else {
toastr.success(`${contactEmail} is unblocked`);
}
} else {
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
// reset to the original value
oldValue = !$(this).prop("checked");
$(this).prop("checked", oldValue);
}
} catch (e) {
toastr.error("Sorry for the inconvenience! Could you refresh the page & retry please?", "Unknown Error");
// reset to the original value
oldValue = !$(this).prop("checked");
$(this).prop("checked", oldValue);
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,128 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Alias Activity{% endblock %}
{% block default_content %}
<h1 class="h3">{{ alias.email }}</h1>
<!-- Stats -->
<div class="row">
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Total</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ total }}</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Forwarded</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ email_forwarded }}</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Replies/Sent</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ email_replied }}</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Blocked</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ email_blocked }}</div>
</div>
</div>
</div>
</div>
<!-- END Stats -->
<div class="row mt-4">
{% for log in logs %}
<div class="col-lg-6">
<div class="my-2 p-2 card border-light">
<div class="font-weight-bold">
{{ log.when | dt }}
<div class="float-right pr-3">
{% if log.bounced %}
⚠️
{% else %}
{% if log.is_reply %}
<i class="fa fa-reply"></i>
{% elif log.blocked %}
<i class="fa fa-ban text-danger"></i>
{% else %}
<i class="fa fa-paper-plane"></i>
{% endif %}
{% endif %}
</div>
</div>
{% if log.bounced and not log.is_reply %}
<div>
<span class="mr-2">{{ log.website_email }}</span>
<img src="{{ url_for('static', filename='arrows/forward-arrow.svg') }}"
class="arrow">
<span class="ml-2">{{ log.alias }}</span>
<img src="{{ url_for('static', filename='arrows/blocked-arrow.svg') }}"
class="arrow">
<span class="ml-2">{{ log.email_log.bounced_mailbox() }}</span>
</div>
{% elif log.bounced and log.is_reply %}
<div>
<span class="ml-2">{{ log.email_log.bounced_mailbox() }}</span>
<img src="{{ url_for('static', filename='arrows/forward-arrow.svg') }}"
class="arrow">
<span class="ml-2">{{ log.alias }}</span>
<img src="{{ url_for('static', filename='arrows/blocked-arrow.svg') }}"
class="arrow">
<span class="mr-2">{{ log.website_email }}</span>
</div>
{% else %}
<div>{{ log.website_email }}</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<nav aria-label="Alias log navigation">
<ul class="pagination">
<li class="page-item">
<a class="btn btn-outline-secondary {% if page_id == 0 %}disabled{% endif %}"
href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id-1) }}">
Previous
</a>
</li>
<li class="page-item">
<a class="btn btn-outline-secondary {% if last_page %}disabled{% endif %}"
href="{{ url_for('dashboard.alias_log', alias_id=alias_id, page_id=page_id+1) }}">
Next
</a>
</li>
</ul>
</nav>
{% endblock %}
{% block script %}{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Receive {{ alias.email }}{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">Receive {{ alias.email }}</h1>
<p>
You are invited to become the owner of the alias <b>{{ alias.email }}</b>
</p>
<p>Please choose the mailbox(es) that owns this alias 👇</p>
<form method="post" class="mt-2">
<select data-width="100%" class="mailbox-select" multiple name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
{{ mailbox.email }}
</option>
{% endfor %}
</select>
<button class="btn btn-success mt-2">Confirm</button>
</form>
</div>
</div>
{% endblock %}
{% block script %}<script>$('.mailbox-select').multipleSelect();</script>{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Send {{ alias.email }}{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">Transfer {{ alias.email }}</h1>
<p>
This page allows you to transfer {{ alias.email }} to another person so they can use it to receive and send
emails.
</p>
{% if alias_transfer_url %}
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ alias_transfer_url }}">
{{ alias_transfer_url }}
</em>
<p class="mt-5">
Please copy the transfer URL. <strong>We won't be able to display it again</strong>. If you need to access it again you can generate a new URL.
</p>
<p class="mt-2">
This transfer URL is <strong>valid for 24 hours</strong>. If it hasn't been used by then it will be automatically disabled.
</p>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="remove">
<button class="btn btn-warning mt-2">Remove alias transfer URL</button>
<div class="small-text">If you don't want to share this alias anymore, you can remove the share URL.</div>
</form>
{% else %}
{% if link_active %}
<p class="alert alert-info">
You have an active transfer link. If you don't want to share this alias anymore, please delete the link.
</p>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="remove">
<button class="btn btn-warning mt-2">Remove alias transfer URL</button>
</form>
{% else %}
<p>
In order to transfer ownership,
please create the <b>Share URL</b> 👇 and send it to the other person.
</p>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<button class="btn btn-primary mt-2">Generate a new alias transfer URL</button>
</form>
{% endif %}
{% endif %}
<p class="mt-5">This person can then confirm the reception and become the owner of the alias.</p>
<div class="alert alert-danger">After the confirmation, you can no longer use this alias.</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,134 @@
{% extends "default.html" %}
{% block title %}API Key{% endblock %}
{% set active_page = "api_key" %}
{% block head %}{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">API Keys</h1>
<div class="alert alert-info">
When you log in on a SimpleLogin mobile app or browser extension,
a new API Key is automatically created and stored on your device.
It's usually named after the device where it was created, e.g. Samsung S8, John's iPhone, etc.
</div>
<div class="alert alert-danger">
API Keys should be kept secret and treated like passwords, they can be used to gain access to your account.
</div>
<div class="row">
{% for api_key in api_keys %}
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ api_key.name or "N/A" }}</h5>
<h6 class="card-subtitle mb-2 text-muted">
{% if api_key.last_used %}
Created {{ api_key.created_at | dt }}.
Used {{ api_key.times }} times.
Was last used {{ api_key.last_used | dt }}.
{% else %}
Never used
{% endif %}
</h6>
<div class="input-group">
<input class="form-control"
id="apikey-{{ api_key.id }}"
readonly
value="**********">
</div>
<br />
<div class="row">
<div class="col">
<form method="post">
<input type="hidden" name="form-name" value="delete">
<input type="hidden" name="api-key-id" value="{{ api_key.id }}">
<span class="card-link btn btn-link float-right text-danger delete-api-key">Delete</span>
</form>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% if api_keys|length > 0 %}
<form method="post">
<input type="hidden" name="form-name" value="delete-all">
<span class="delete btn btn-outline-danger delete-all-api-keys float-right">
Delete All &nbsp; &nbsp; <i class="fe fe-trash"></i>
</span>
</form>
<br />
{% endif %}
<hr />
<form method="post">
{{ new_api_key_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<h2 class="h4">New API Key</h2>
{{ new_api_key_form.name(class="form-control", placeholder="Chrome") }}
{{ render_field_errors(new_api_key_form.name) }}
<div class="small-text">Name of the api key, e.g. where it will be used.</div>
<button class="btn btn-success mt-2">Create</button>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-api-key").on("click", function (e) {
let that = $(this);
bootbox.confirm({
message: "If this API Key is currently in use, you might need to login again on the corresponding device, please confirm.",
buttons: {
confirm: {
label: 'Yes, delete it',
className: 'btn-danger'
},
cancel: {
label: 'Cancel',
className: 'btn-outline-primary'
}
},
callback: function (result) {
if (result) {
that.closest("form").submit();
}
}
})
});
$(".delete-all-api-keys").on("click", function (e) {
let that = $(this);
bootbox.confirm({
message: "This will delete all API Keys, they will all stop working, are you sure?",
buttons: {
confirm: {
label: 'Delete All',
className: 'btn-danger'
},
cancel: {
label: 'Cancel',
className: 'btn-outline-primary'
}
},
callback: function (result) {
if (result) {
that.closest("form").submit();
}
}
})
});
</script>
{% endblock %}

View File

@ -0,0 +1,73 @@
{% extends "default.html" %}
{% set active_page = "app" %}
{% block title %}Sign in with SimpleLogin apps{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">Apps</h1>
<div class="small-text">
List of websites/apps that you have used <b>Sign in with SimpleLogin</b>
</div>
</div>
</div>
<div class="row row-cards row-deck mt-4">
<div class="col-12">
<div class="card">
<div class="table-responsive">
<table class="table table-hover table-outline table-vcenter text-nowrap card-table">
<thead>
<tr>
<th>App</th>
<th>
Info
<i class="fe fe-help-circle"
data-toggle="tooltip"
title="Info this app/website receives"></i>
</th>
<th>
First used
<i class="fe fe-help-circle"
data-toggle="tooltip"
title="The first time you have used the 'Sign in with SimpleLogin' button on this app/website"></i>
</th>
<th>Actions</th>
<!--<th class="text-center">Last used</th>-->
</tr>
</thead>
<tbody>
{% for client_user in client_users %}
<tr>
<td>{{ client_user.client.name }}</td>
<td>
{% for scope, val in client_user.get_user_info().items() %}
<div>
{% if scope == "email" %}
Email:
<a href="mailto:{{ val }}">{{ val }}</a>
{% elif scope == "name" %}
Name: {{ val }}
{% endif %}
</div>
{% endfor %}
</td>
<td>{{ client_user.created_at | dt }}</td>
<td>
<form method="post">
<input type="hidden" name="client-user-id" value="{{ client_user.id }}">
<button class="btn btn-warning">Remove</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,72 @@
{% extends "default.html" %}
{% set active_page = "setting" %}
{% block title %}Alias Batch Import{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">Alias Batch Import</h1>
<div class="alert alert-primary">
Only aliases created with <b>your verified domains</b> can be imported.
<br />
If mailboxes are set for an alias, they will only be linked if they already exist.
<br />
Please make sure to use the csv template file.
</div>
<p>
The import can take several minutes.
Please come back to this page to verify the import status.
<br />
If an alias already exists, it won't be imported.
</p>
<a href="{{ url_for('static', filename='batch_import_template.csv') }}"
download>Download CSV Template</a>
<hr />
<form method="post" enctype="multipart/form-data" class="mt-4">
{{ csrf_form.csrf_token }}
<input required
type="file"
name="alias-file"
accept=".csv"
class="form-control-file">
<label>
Only <b>.csv</b> file is supported.
</label>
<br />
<button class="btn btn-success mt-2">Upload</button>
</form>
{% if batch_imports %}
<hr />
<h2 class="h3 mt-7">Batch imports</h2>
<table class="table">
<thead>
<tr>
<th scope="col">Uploaded</th>
<th scope="col">Number Alias Imported</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
{% for batch_import in batch_imports %}
<tr>
<td>{{ batch_import.created_at | dt }}</td>
<td>{{ batch_import.nb_alias() }}</td>
<td>
{% if batch_import.processed %}
Processed ✅
{% else %}
Pending
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,108 @@
{% extends "default.html" %}
{% block title %}Billing{% endblock %}
{% block head %}{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3 mb-5">Billing</h1>
{% if sub.cancelled %}
<p>
You are on the <b>{{ sub.plan_name() }}</b> plan.
<br />
You have canceled your subscription and it will end on {{ sub.next_bill_date.strftime("%Y-%m-%d") }}
</p>
<p class="alert alert-info">
If you change your mind you can subscribe again to SimpleLogin but
please note that this will be a completely
new subscription and
your payment method will be charged <b>immediately</b>.
<br />
The period left in the current subscription isn't taken into account.
<br />
<a href="{{ url_for('dashboard.pricing') }}"
class="btn btn-primary mt-2">Re-subscribe</a>
</p>
{% else %}
<p>
You are on the <b>{{ sub.plan_name() }}</b> plan. Thank you very much for supporting
SimpleLogin. 🙌
<br />
The next billing cycle starts at {{ sub.next_bill_date.strftime("%Y-%m-%d") }}.
</p>
<div class="mt-3">
Click here to update billing information on Paddle, our payment partner:
<br />
<a class="btn btn-outline-success mt-2" href="{{ sub.update_url }}">Update billing information</a>
</div>
<hr />
<div class="mt-6">
<h4>Change Plan</h4>
You can change the plan at any moment.
<br />
Please note that the new billing cycle starts instantly
i.e. you will be charged <b>immediately</b> the annual fee ($30) when switching from monthly plan or vice-versa
<b>without pro rata computation </b>.
<br />
To change the plan you can also cancel the current one and subscribe a new one <b>by the end</b> of this plan.
{% if sub.plan == PlanEnum.yearly %}
<form method="post"
onsubmit="return confirm('This will change your current subscription plan, please confirm');">
<input type="hidden" name="form-name" value="change-monthly">
<button class="btn btn-outline-primary mt-2">Change to Monthly Plan</button>
</form>
{% else %}
<form method="post"
onsubmit="return confirm('This will change your current subscription plan, please confirm');">
<input type="hidden" name="form-name" value="change-yearly">
<button class="btn btn-outline-primary mt-2">Change to Yearly Plan</button>
</form>
{% endif %}
</div>
<hr />
<div>
<h4>Cancel subscription</h4>
Don't want to protect your inbox anymore?
<br />
<form method="post">
<input type="hidden" name="form-name" value="cancel">
<span class="cancel btn btn-outline-danger mt-2">
Cancel subscription <i class="fe fe-alert-triangle text-danger"></i>
</span>
</form>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".cancel").on("click", function (e) {
let that = $(this);
bootbox.confirm({
message: "This operation is irreversible, please confirm.",
buttons: {
confirm: {
label: 'Cancel subscription',
className: 'btn-danger'
},
cancel: {
label: 'Close',
className: 'btn-outline-primary'
}
},
callback: function (result) {
if (result) {
that.closest("form").submit();
}
}
})
});
</script>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Block a sender{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">Block sender</h1>
<p>
You are about to block the sender <b>{{ contact.website_email }}</b> from sending emails to
<b>{{ contact.alias.email }}</b>
</p>
<form method="post">
<button class="btn btn-warning">Confirm</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,66 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Contact {{ contact.email }} - Alias {{ alias.email }}{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id) }}">{{ alias.email }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
{{ contact.email }}
{% if contact.pgp_finger_print %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="PGP Enabled">🗝</span>
{% endif %}
</li>
</ol>
</nav>
</h1>
<div class="card">
<form method="post">
{{ pgp_form.csrf_token }}
<input type="hidden" name="form-name" value="pgp">
<div class="card-body">
<div class="card-title">
Pretty Good Privacy (PGP)
<div class="small-text">
By importing your contact PGP Public Key into SimpleLogin, all emails sent to
<b>{{ contact.email }}</b> from your alias <b>{{ alias.email }}</b>
are <b>encrypted</b>.
</div>
</div>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
{% endif %}
<div class="form-group">
<label class="form-label">PGP Public Key</label>
<textarea name="pgp" {% if not current_user.is_premium() %} disabled {% endif %} class="form-control" rows=10 id="pgp-public-key" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ contact.pgp_public_key or "" }}</textarea>
</div>
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}
disabled {% endif %} value="save">
Save
</button>
{% if contact.pgp_finger_print %}
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
{% endif %}
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script src="/static/js/utils/drag-drop-into-text.js"></script>
<script>enableDragDropForPGPKeys('#pgp-public-key');</script>
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block head %}
<script src="https://cdn.paddle.com/paddle/paddle.js"></script>
<script>
if (window.Paddle === undefined) {
console.log("cannot load Paddle from CDN");
document.write('<script src="/static/vendor/paddle.js"><\/script>')
}
</script>
<style type="text/css">
html.mvc__a.mvc__lot.mvc__of.mvc__classes.mvc__to.mvc__increase.mvc__the.mvc__odds.mvc__of.mvc__winning.mvc__specificity, html.mvc__a.mvc__lot.mvc__of.mvc__classes.mvc__to.mvc__increase.mvc__the.mvc__odds.mvc__of.mvc__winning.mvc__specificity > body {
position: static;
}
</style>
{% endblock %}
{% block title %}Coupon{% endblock %}
{% block default_content %}
{% if can_use_coupon %}
<div class="card">
<div class="card-body">
<h1 class="h2">Coupon</h1>
<div class="mb-4">Please enter the coupon here to upgrade your account or extend your current subscription.</div>
<form method="post">
{{ coupon_form.csrf_token }}
{{ coupon_form.code(class="form-control", placeholder="Licence Code") }}
{{ render_field_errors(coupon_form.code) }}
<button class="btn btn-success mt-2">Apply</button>
</form>
</div>
</div>
{% endif %}
<div class="card">
<div class="card-body">
<h2>1-year coupon</h2>
<div class="mb-3">
You can buy a 1-year coupon that allows anyone to have the SimpleLogin premium for 1 year.
Can be an idea for a gift card :).
After the payment, the coupon will be sent to you by email.
</div>
<div class="alert alert-info">The coupon must be used before {{ max_coupon_date.date().isoformat() }}</div>
<a href="#!" class="paddle_button btn btn-primary" data-product="{{ PADDLE_COUPON_ID }}">
Buy 1-year SimpleLogin
coupon
</a>
</div>
</div>
{% endblock %}
{% block script %}<script type="text/javascript">Paddle.Setup({vendor: {{PADDLE_VENDOR_ID }} });</script>{% endblock %}

View File

@ -0,0 +1,134 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Custom Alias{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">New Custom Alias</h1>
{% if user_custom_domains|length == 0 and not DISABLE_ALIAS_SUFFIX %}
<div class="row">
<div class="col p-1">
<div class="alert alert-primary" role="alert">
You might notice a random word after the dot(<em>.</em>) in the alias.
This part is to avoid a person taking all the "nice" aliases like
<b>hello@{{ FIRST_ALIAS_DOMAIN }}</b>,
<b>me@{{ FIRST_ALIAS_DOMAIN }}</b>, etc.
<br />
If you add your own domain, this restriction is removed, and you can fully customize the alias.
<br />
</div>
</div>
</div>
{% endif %}
<form method="post" data-parsley-validate>
<div class="row mb-2">
<div class="col-sm-6 mb-1 p-1" style="min-width: 4em">
<input name="prefix"
class="form-control"
id="prefix"
type="text"
data-parsley-pattern="[0-9a-z-_.]{1,}"
data-parsley-trigger="change"
data-parsley-error-message="Only lowercase letters, dots, numbers, dashes (-) and underscores (_) are currently supported."
maxlength="40"
placeholder="Alias prefix, for example newsletter.com-123_xyz"
autofocus
required>
</div>
<div class="col-sm-6 p-1">
<select class="form-control" name="signed-alias-suffix">
{% for alias_suffix in alias_suffixes %}
<option value="{{ alias_suffix.signed_suffix }}" {% if alias_suffix.is_premium %}
title="Only available to Premium accounts" {% elif not alias_suffix.is_custom and at_least_a_premium_domain %} title="Available to all accounts" {% endif %}>
{% if alias_suffix.is_custom %}
{% if alias_suffix.mx_verified %}
{{ alias_suffix.suffix }} (your domain)
{% else %}
{{ alias_suffix.suffix }} (your domain, not MX verified yet)
{% endif %}
{% else %}
{% if alias_suffix.is_premium %}
{{ alias_suffix.suffix }} (Premium domain)
{% else %}
{{ alias_suffix.suffix }} (Public domain)
{% endif %}
{% endif %}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="row mb-2">
<div class="col p-1">
<select data-width="100%"
class="mailbox-select"
id="mailboxes"
multiple
name="mailboxes"
required>
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
{{ mailbox.email }}
</option>
{% endfor %}
</select>
<div class="small-text">The mailbox(es) that owns this alias.</div>
</div>
</div>
<div class="row mb-2">
<div class="col p-1">
<textarea name="note"
class="form-control"
rows="3"
placeholder="Note, can be anything to help you remember why you created this alias. This field is optional."></textarea>
</div>
</div>
<div class="row">
<div class="col p-1">
<button type="submit" id="create" class="btn btn-primary mt-1">Create</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$('.mailbox-select').multipleSelect();
// Ctrl-enter submit the form
$('form').keydown(function (event) {
if (event.ctrlKey && event.keyCode === 13) {
$("#submit").click();
}
})
$("#create").on("click", async function () {
let that = $(this);
let mailbox_ids = $(`#mailboxes`).val();
let prefix = $('#prefix').val();
if (mailbox_ids.length == 0) {
toastr.error("You must select at least a mailbox", "Error");
return;
}
if (!prefix) {
toastr.error("Alias cannot be empty", "Error");
return;
}
that.closest("form").submit();
})
</script>
{% endblock %}

View File

@ -0,0 +1,101 @@
{% extends "default.html" %}
{% set active_page = "custom_domain" %}
{% block title %}Custom Domains{% endblock %}
{% block head %}{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">
Custom Domains
<a class="ml-3 text-info"
style="font-size: 12px"
data-toggle="collapse"
href="#howtouse"
role="button"
aria-expanded="false"
aria-controls="collapseExample">
How to use <i class="fe fe-chevrons-down"></i>
</a>
</h1>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">
This feature is only available on Premium plan.
<a href="{{ URL }}/dashboard/pricing" target="_blank" rel="noopener">
Upgrade<i class="fe fe-external-link"></i>
</a>
</div>
{% endif %}
<div class="alert alert-primary collapse {% if not custom_domains %} show{% endif %}"
id="howtouse"
role="alert">
By adding your domain, you can create aliases like <b>hi@my-domain.com</b>
<br />
You can also enable <b>catch-all</b> to create aliases on-the-fly:
simply use <b>anything@my-domain.com</b> and it'll be created when
it receives an email.
</div>
<div class="row">
{% for custom_domain in custom_domains %}
<div class="col-12 col-lg-6">
<div class="card" style="">
<div class="card-body">
<h5 class="card-title">
<a href="{{ url_for('dashboard.domain_detail', custom_domain_id=custom_domain.id) }}">{{ custom_domain.domain }}</a>
{% if custom_domain.ownership_verified and not custom_domain.verified %}
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id, _anchor='dns-setup') }}"
class="btn btn-info btn-sm">
Ownership verified. Setup the DNS
</a>
{% elif custom_domain.ownership_verified and custom_domain.verified %}
<span class="badge badge-success">Domain ready</span>
<!-- custom_domain.ownership_verified is False -->
{% else %}
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id, _anchor='ownership-form') }}"
class="btn btn-warning btn-sm"
role="button">
Verify domain ownership
</a>
{% endif %}
</h5>
<h6 class="card-subtitle mb-4 text-muted">
Created {{ custom_domain.created_at | dt }}
<br />
<span class="font-weight-bold">{{ custom_domain.nb_alias() }}</span> aliases.
<br />
</h6>
<a href="{{ url_for('dashboard.domain_detail', custom_domain_id=custom_domain.id) }}"
class="mt-3">Details ➡</a>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
<form method="post" class="mt-2">
{{ new_custom_domain_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<h2 class="h4">New Domain</h2>
{{ new_custom_domain_form.domain(class="form-control", placeholder="my-domain.com", maxlength=128) }}
{{ render_field_errors(new_custom_domain_form.domain) }}
<div class="small-text">
Please use full path domain, for example <b>my-domain.com</b>
or <b>my-subdomain.my-domain.com</b> if you are using a subdomain.
</div>
<button class="btn btn-primary mt-2">Create</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}<script>$('.mailbox-select').multipleSelect();</script>{% endblock %}

View File

@ -0,0 +1,51 @@
{% extends "default.html" %}
{% set active_page = "setting" %}
{% block title %}Delete account{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<div class="h2">Account Deletion</div>
<div class="my-3 alert alert-warning">
Once an account is deleted, it can't be restored.
All its records (aliases, domains, settings, etc.) are immediately deleted.
</div>
<form method="post">
<input type="hidden" name="form-name" value="delete-account">
{{ delete_form.csrf_token }}
<span class="delete-account btn btn-outline-danger">Delete account</span>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-account").on("click", function (e) {
let that = $(this);
bootbox.confirm({
message: "All your data including your aliases will be deleted, " +
"other people might not be able to reach you after, " +
" please confirm.",
buttons: {
confirm: {
label: 'Yes, delete my account',
className: 'btn-danger'
},
cancel: {
label: 'Cancel',
className: 'btn-outline-primary'
}
},
callback: function (result) {
if (result) {
that.closest("form").submit();
}
}
})
});
</script>
{% endblock %}

View File

@ -0,0 +1,218 @@
{% extends "default.html" %}
{% set active_page = "directory" %}
{% block title %}Directory{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">
Directories
<a class="ml-3 text-info"
style="font-size: 12px"
data-toggle="collapse"
href="#howtouse"
role="button"
aria-expanded="false"
aria-controls="collapseExample">
How to use <i class="fe fe-chevrons-down"></i>
</a>
</h1>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
{% endif %}
<div class="alert alert-primary collapse {% if not dirs %} show{% endif %}"
id="howtouse"
role="alert">
<div>
Directory allows you to create aliases <b>on the fly</b>.
</div>
<div class="mt-2 pb-2">
1⃣ Pick a name for your directory, says <em>my_directory</em>
<br />
2⃣ Quickly use one of the following formats to create an alias on-the-fly <b>without creating this alias
beforehand</b>
</div>
<div class="pl-3 py-2 bg-secondary">
<em>my_directory/<b>anything</b>@{{ FIRST_ALIAS_DOMAIN }}</em> or
<br />
<em>my_directory+<b>anything</b>@{{ FIRST_ALIAS_DOMAIN }}</em> or
<br />
<em>my_directory#<b>anything</b>@{{ FIRST_ALIAS_DOMAIN }}</em>
<br />
</div>
<em><b>anything</b></em> is any string composed of lowercase characters.
<br />
You can find more info on directory on our
<a href="https://simplelogin.io/blog/alias-directory/">blog post</a>
.
<div class="mt-2">
You can use this feature on the following domains:
{% for alias_domain in ALIAS_DOMAINS %}<div class="font-weight-bold">{{ alias_domain }}</div>{% endfor %}
</div>
<div class="h4 text-primary mt-3">
The alias will be created the first time it receives an email.
</div>
</div>
<div class="row">
{% for dir in dirs %}
<div class="col-12 col-lg-6">
<div class="card" style="">
<div class="card-body">
<h5 class="card-title">
<div class="d-flex">
{{ dir.name }}
<form method="post">
{{ toggle_dir_form.csrf_token }}
<input type="hidden" name="form-name" value="toggle-directory">
{{ toggle_dir_form.directory_id( type="hidden", value=dir.id) }}
<label class="custom-switch cursor" style="padding-left: 1rem" data-toggle="tooltip" {% if dir.disabled %}
title="Enable directory on-the-fly alias creation" {% else %} title="Disable directory on-the-fly alias creation" {% endif %}>
{{ toggle_dir_form.directory_enabled( class="custom-switch-input", checked=(not dir.disabled) ) }}
<span class="custom-switch-indicator"></span>
</label>
</form>
</div>
</h5>
<h6 class="card-subtitle mb-2 text-muted">
{% if dir.disabled %}
<div class="mb-3">⚠️ On-the-fly alias creation is disabled, you can't create new aliases with this directory.</div>
{% endif %}
Created {{ dir.created_at | dt }}
<br />
<span class="font-weight-bold">{{ dir.nb_alias() }}</span> aliases.
<br />
<br />
<b>Mailboxes:</b> <i class="fe fe-info"
data-toggle="tooltip"
title="Aliases created with this directory are automatically owned by these mailboxes"></i>
<br />
{% set dir_mailboxes=dir.mailboxes %}
<form method="post" class="mt-2">
{{ update_dir_form.csrf_token }}
<input type="hidden" name="form-name" value="update">
{{ update_dir_form.directory_id( type="hidden", value=dir.id) }}
<select data-width="100%"
required
class="mailbox-select"
multiple
name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox in dir_mailboxes %} selected{% endif %}>
{{ mailbox.email }}
</option>
{% endfor %}
</select>
<button class="mt-2 btn btn-outline-primary btn-sm">Update</button>
</form>
</h6>
</div>
<div class="card-footer p-0">
<div class="row">
<div class="col">
<form method="post">
{{ delete_dir_form.csrf_token }}
<input type="hidden" name="form-name" value="delete">
{{ delete_dir_form.directory_id( type="hidden", value=dir.id) }}
<span class="card-link btn btn-link float-right text-danger delete-dir">Delete</span>
</form>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row {% if current_user.directory_quota <= 0 %} disabled-content{% endif %}">
<div class="col">
<div class="card">
<div class="card-body">
<form method="post" class="mt-2">
{{ new_dir_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<h2 class="h4 mb-1">New Directory</h2>
<div class="small-text mb-4">You can create up to {{ current_user.directory_quota }} directories.</div>
{{ new_dir_form.name(class="form-control", placeholder="my-directory",
pattern="[0-9a-z-_]{3,}",
title="Only letter, number, dash (-), underscore (_) can be used. Directory name must be at least 3 characters.") }}
{{ render_field_errors(new_dir_form.name) }}
<div class="small-text">
Directory name must be at least 3 characters.
Only lowercase letters, numbers, dashes (-) and underscores (_) are currently supported.
</div>
<div class="mt-3 small-text alert alert-info">
By default, aliases created with directory are "owned" by your default
mailbox <b>{{ current_user.default_mailbox.email }}</b>.
<br />
You can however choose the mailbox(es) that new alias automatically belongs to by setting this below
option.
</div>
<select data-width="100%" class="mailbox-select" multiple name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
{{ mailbox.email }}
</option>
{% endfor %}
</select>
<button id="btn-create-directory" class="btn btn-primary mt-2">Create</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-dir").on("click", function (e) {
let directory_name = $(this).parent().find("#directory_name").val();
const element = document.createElement('div');
element.innerText = directory_name;
const sanitized_name = element.innerHTML;
const message = `All aliases associated with <b>${sanitized_name}</b> directory will also be deleted. ` +
`As a deleted directory can't be used by someone else, deleting a directory doesn't reset your directory quota. ` +
`Your directory quota will be {{ current_user.directory_quota }} after the deletion, ` +
" please confirm.";
bootbox.confirm({
message: message,
buttons: {
confirm: {
label: 'Yes, delete it',
className: 'btn-danger'
},
cancel: {
label: 'Cancel',
className: 'btn-outline-primary'
}
},
callback: (result) => {
if (result) {
this.closest("form").submit();
}
}
})
});
$('.mailbox-select').multipleSelect();
$(".custom-switch-input").change(function (e) {
$(this).closest("form").submit();
});
</script>
{% endblock %}

View File

@ -0,0 +1,138 @@
{% extends "dashboard/domain_detail/base.html" %}
{% set domain_detail_page = "auto_create" %}
{% block title %}{{ custom_domain.domain }} Auto Create Rules{% endblock %}
{% block domain_detail_content %}
<h1 class="h2 mb-1">{{ custom_domain.domain }} auto create alias rules</h1>
<div>
<span class="badge badge-info">Advanced</span>
<span class="badge badge-warning">Beta</span>
</div>
{% if custom_domain.catch_all %}
<div class="alert alert-warning mt-3">Rules are ineffective when catch-all is enabled.</div>
{% endif %}
<div class="{% if custom_domain.catch_all %} disabled-content{% endif %}">
<div class="mt-3 mb-2">
For a greater control than a simple catch-all, you can define a set of <b>rules</b> to auto create aliases.
<br />
A rule is based on a regular expression (<b>regex</b>): if an alias <b>fully</b> matches the expression,
it'll be automatically created.
</div>
<div class="alert alert-info">
Only the <b>local</b> part of the alias (i.e. <b>@{{ custom_domain.domain }}</b> is ignored) during the
regex test.
</div>
<div class="alert alert-info">When there are several rules, rules will be evaluated by their order.</div>
{% if custom_domain.auto_create_rules | length > 0 %}
<div class="mt-2" id="rule-list">
{% for auto_create_rule in custom_domain.auto_create_rules %}
<div class="card">
<div class="card-body">
Order: <b>{{ auto_create_rule.order }}</b>
<br />
<input readonly value="{{ auto_create_rule.regex }}" class="form-control">
New alias will belong to
{% for mailbox in auto_create_rule.mailboxes %}
<b>{{ mailbox.email }}</b>
{% if not loop.last %},{% endif %}
{% endfor %}
<form method="post" class="mt-2">
<input type="hidden" name="form-name" value="delete-auto-create-rule">
<input type="hidden" name="rule-id" value="{{ auto_create_rule.id }}">
<button class="btn btn-outline-danger btn-sm float-right">Delete</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<div class="mt-2" id="new-rule">
<hr />
<h3>New rule</h3>
<form method="post" action="#rule-list" data-parsley-validate>
<input type="hidden" name="form-name" value="create-auto-create-rule">
{{ new_auto_create_rule_form.csrf_token }}
<div class="form-group">
<label>Regex</label>
{{ new_auto_create_rule_form.regex(class="form-control",
placeholder="prefix.*"
) }}
{{ render_field_errors(new_auto_create_rule_form.regex) }}
<div class="small-text">
For example, if you want aliases that starts with <b>prefix</b> to be automatically created, you can set
the
regex to <em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="prefix.*">prefix.*</em>
<br />
If you want aliases that ends with <b>suffix</b> to be automatically created, you can use the regex
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text=".*suffix">.*suffix</em>
<br />
To test out regex, we recommend using regex tester tool like
<a href="https://regex101.com" target="_blank">https://regex101.com↗</a>
</div>
</div>
<div class="form-group">
<label>Order</label>
{{ new_auto_create_rule_form.order(class="form-control", placeholder="10", min=1, value=1, type="number") }}
{{ render_field_errors(new_auto_create_rule_form.order) }}
</div>
<div class="form-group">
<div class="flex-grow-1 mr-2">
<select data-width="100%"
required
class="mailbox-select"
multiple
name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox.id == current_user.default_mailbox_id %} selected{% endif %}>
{{ mailbox.email }}
</option>
{% endfor %}
</select>
</div>
</div>
<button class="btn btn-primary mt-2">Create</button>
</form>
</div>
<div id="debug-zone">
<hr />
<h3>Debug Zone</h3>
<p>You can test whether an alias will be automatically created given the rules above</p>
<div class="alert alert-info">No worries, no alias will be created during the test :)</div>
<form method="post" action="#debug-zone">
<input type="hidden" name="form-name" value="test-auto-create-rule">
{{ auto_create_test_form.csrf_token }}
<div class="d-flex">
<div class="form-group d-flex">
{{ auto_create_test_form.local(class="form-control", type="text", placeholder="local", value=auto_create_test_local) }}
{{ render_field_errors(auto_create_test_form.local) }}
<b class="pt-2 ml-1">@{{ custom_domain.domain }}</b>
</div>
</div>
<div class="form-group">
<button class="btn btn-outline-primary">Test</button>
</div>
</form>
{% if auto_create_test_result %}
<div class="alert {% if auto_create_test_passed %}
alert-success {% else %} alert-warning {% endif %}">
{{ auto_create_test_result }}
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block script %}<script>$('.mailbox-select').multipleSelect();</script>{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends "default.html" %}
{% if custom_domain.is_sl_subdomain %}
{% set active_page = "subdomain" %}
{% else %}
{% set active_page = "custom_domain" %}
{% endif %}
{% block default_content %}
<div class="row">
<div class="col-lg-3 order-lg-1 mb-4">
<div class="list-group list-group-transparent mb-0">
<a href="{{ url_for('dashboard.domain_detail', custom_domain_id=custom_domain.id) }}"
class="list-group-item list-group-item-action {{ 'active' if domain_detail_page == 'info' }}">
<span class="icon mr-3"><i class="fe fe-flag"></i></span>Info
</a>
{% if not custom_domain.is_sl_subdomain %}
<a href="{{ url_for('dashboard.domain_detail_dns', custom_domain_id=custom_domain.id) }}"
class="list-group-item list-group-item-action {{ 'active' if domain_detail_page == 'dns' }}">
<span class="icon mr-3"><i class="fe fe-cloud"></i></span>DNS
</a>
{% endif %}
<a href="{{ url_for('dashboard.domain_detail_trash', custom_domain_id=custom_domain.id) }}"
class="list-group-item list-group-item-action {{ 'active' if domain_detail_page == 'trash' }}">
<span class="icon mr-3"><i class="fe fe-trash"></i></span>Deleted Alias
</a>
<a href="{{ url_for('dashboard.domain_detail_auto_create', custom_domain_id=custom_domain.id) }}"
class="list-group-item list-group-item-action {{ 'active' if domain_detail_page == 'auto_create' }}">
<span class="icon mr-3"><i class="fe fe-layers"></i></span>Auto Create
</a>
</div>
</div>
<div class="col-lg-9">
<div class="card">
<div class="card-body">
<div class="text-wrap p-lg-6 domain_detail_content">
{% block domain_detail_content %}{% endblock %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,409 @@
{% extends "dashboard/domain_detail/base.html" %}
{% set domain_detail_page = "dns" %}
{% block title %}{{ custom_domain.domain }} DNS{% endblock %}
{% block domain_detail_content %}
<div class="p-4 mr-auto" style="max-width: 60rem;">
<h1 class="h2">{{ custom_domain.domain }}</h1>
<div>Please follow the steps below to set up your domain.</div>
<div class="small-text mb-5">DNS changes could take up to 24 hours to update.</div>
{% if not custom_domain.ownership_verified %}
<div id="ownership-form">
<div class="font-weight-bold">
Domain ownership verification
{% if custom_domain.ownership_verified %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="Domain Ownership Verified"></span>
{% else %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="Domain Ownership Required">🚫</span>
{% endif %}
</div>
{% if not custom_domain.ownership_verified %}
<div class="mb-2">
To verify ownership of the domain, please add the following TXT record.
Some domain registrars (Namecheap, CloudFlare, etc) might use <em>@</em> for the root domain.
</div>
<div class="mb-3 p-3 dns-record">
Record: TXT
<br />
Domain: {{ custom_domain.domain }} or <b>@</b>
<br />
Value: <em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ custom_domain.get_ownership_dns_txt_value() }}">{{ custom_domain.get_ownership_dns_txt_value() }}</em>
</div>
<form method="post" action="#ownership-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-ownership">
<button type="submit" class="btn btn-primary">Verify</button>
</form>
{% if not ownership_ok %}
<div class="text-danger mt-4">
Your DNS is not correctly set. The TXT record we obtain is:
<div class="mb-3 p-3 dns-record">
{% if not ownership_errors %}(Empty){% endif %}
{% for r in ownership_errors %}
{{ r }}
<br />
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endif %}
<hr />
{% endif %}
<div class="{% if not custom_domain.ownership_verified %} disabled-content{% endif %}"
id="dns-setup">
{% if not custom_domain.ownership_verified %}
<div class="alert alert-warning">A domain ownership must be verified first.</div>
{% endif %}
<div id="mx-form">
<div class="font-weight-bold">
1. MX record
{% if custom_domain.verified %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="MX Record Verified"></span>
{% else %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="MX Record Not Verified">🚫</span>
{% endif %}
</div>
<div class="mb-2">
Add the following MX DNS record to your domain.
<br />
Please note that there's a dot (<em>.</em>) at the end target addresses.
If your domain registrar doesn't allow this trailing dot, please remove it when adding the DNS record.
<br />
Some domain registrars (Namecheap, CloudFlare, etc) might also use <em>@</em> for the root domain.
</div>
{% for priority, email_server in EMAIL_SERVERS_WITH_PRIORITY %}
<div class="mb-3 p-3 dns-record">
Record: MX
<br />
Domain: {{ custom_domain.domain }} or
<b>@</b>
<br />
Priority: {{ priority }}
<br />
Target: <em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ email_server }}">{{ email_server }}</em>
</div>
{% endfor %}
<form method="post" action="#mx-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-mx">
{% if custom_domain.verified %}
<button type="submit" class="btn btn-outline-primary">Re-verify</button>
{% else %}
<button type="submit" class="btn btn-primary">Verify</button>
{% endif %}
</form>
{% if not mx_ok %}
<div class="text-danger mt-4">
Your DNS is not correctly set. The MX record we obtain is:
<div class="mb-3 p-3 dns-record">
{% if not mx_errors %}(Empty){% endif %}
{% for r in mx_errors %}
{{ r }}
<br />
{% endfor %}
</div>
{% if custom_domain.verified %}
<div class="alert alert-danger">
Without the MX record set up correctly, you can miss emails sent to your aliases.
Please update the MX record ASAP.
</div>
{% endif %}
</div>
{% endif %}
</div>
<hr />
<div id="spf-form">
<div class="font-weight-bold">
2. SPF (Optional)
{% if custom_domain.spf_verified %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="SPF Verified"></span>
{% else %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="SPF Not Verified">🚫</span>
{% endif %}
</div>
<div>
SPF
<a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework"
target="_blank"
rel="noopener">(Wikipedia↗)</a>
is an email
authentication method
designed to detect forging sender addresses during the delivery of the email.
<br />
Setting up SPF is highly recommended to reduce the chance your emails ending up in the recipient's Spam
folder.
</div>
<div class="mb-2">Add the following TXT DNS record to your domain.</div>
<div class="mb-2 p-3 dns-record">
Record: TXT
<br />
Domain: {{ custom_domain.domain }} or
<b>@</b>
<br />
Value:
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ spf_record }}">
{{ spf_record }}
</em>
</div>
<form method="post" action="#spf-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-spf">
{% if custom_domain.spf_verified %}
<button type="submit" class="btn btn-outline-primary">Re-verify</button>
{% else %}
<button type="submit" class="btn btn-primary">Verify</button>
{% endif %}
</form>
{% if not spf_ok %}
<div class="text-danger mt-4">
Your DNS is not correctly set. The TXT record we obtain is:
<div class="mb-3 p-3 dns-record">
{% if not spf_errors %}(Empty){% endif %}
{% for r in spf_errors %}
{{ r }}
<br />
{% endfor %}
</div>
{% if custom_domain.spf_verified %}
Without SPF setup, emails you sent from your alias might end up in Spam/Junk folder.
{% endif %}
</div>
{% endif %}
</div>
<hr />
<div id="dkim-form">
<div class="font-weight-bold">
3. DKIM (Optional)
{% if custom_domain.dkim_verified %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="SPF Verified"></span>
{% else %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="DKIM Not Verified">🚫</span>
{% endif %}
</div>
<div>
DKIM
<a href="https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail"
target="_blank"
rel="noopener">(Wikipedia↗)</a>
is an
email
authentication method
designed to avoid email spoofing.
<br />
Setting up DKIM is highly recommended to reduce the chance your emails ending up in the recipient's Spam
folder.
</div>
<div class="mb-2">
Add the following CNAME DNS records to your domain.
</div>
{% for dkim_prefix, dkim_cname_value in dkim_records %}
<div class="mb-2 p-3 dns-record">
Record: CNAME
<br />
Domain: <em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ dkim_prefix }}">{{ dkim_prefix }}</em>
<br />
Value:
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ dkim_cname_value }}."
style="overflow-wrap: break-word">
{{ dkim_cname_value }}.
</em>
</div>
{% endfor %}
<div class="alert alert-info">
Some DNS registrar might require a full record path, in this case please use
<i>dkim._domainkey.{{ custom_domain.domain }}</i> as domain value instead.
<br />
If you are using a subdomain, e.g. <i>subdomain.domain.com</i>,
you need to use <i>dkim._domainkey.subdomain</i> as domain value instead.
<br />
</div>
<div class="alert alert-info">
If you are using CloudFlare, please make sure to <b>not</b> select the Proxy option.
<br />
<br />
<img src="/static/images/cloudflare-proxy.png" class="w-100">
</div>
<form method="post" action="#dkim-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-dkim">
{% if custom_domain.dkim_verified %}
<button type="submit" class="btn btn-outline-primary">
Re-verify
</button>
{% else %}
<button type="submit" class="btn btn-primary">
Verify
</button>
{% endif %}
</form>
{% if not dkim_ok %}
<div class="text-danger mt-4">
<p>
Your DNS is not correctly set.
</p>
<ul>
{% for custom_record, retrieved_cname in dkim_errors.items() %}
<li>
The CNAME record we obtain for <em>{{ custom_record }}</em> is {{ retrieved_cname }}
</li>
{% endfor %}
</ul>
{% if custom_domain.dkim_verified %}
Without DKIM setup, emails you sent from your alias might end up in Spam/Junk folder.
{% endif %}
</div>
{% if custom_domain.dkim_verified %}
<div class="text-danger mt-4">
DKIM is still enabled. Please update your DKIM settings with all CNAME records
</div>
{% endif %}
{% endif %}
</div>
<hr />
<div id="dmarc-form">
<div class="font-weight-bold">
4. DMARC (Optional)
{% if custom_domain.dmarc_verified %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="DMARC Verified"></span>
{% else %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="DMARC Not Verified">🚫 </span>
{% endif %}
</div>
<div>
DMARC
<a href="https://en.wikipedia.org/wiki/DMARC"
target="_blank"
rel="noopener">
(Wikipedia↗)
</a>
is designed to protect the domain from unauthorized use, commonly known as email spoofing.
<br />
Built around SPF and DKIM, a DMARC policy tells the receiving mail server what to do if
neither of those authentication methods passes.
</div>
<div class="mb-2">
Add the following TXT DNS record to your domain.
</div>
<div class="mb-2 p-3 dns-record">
Record: TXT
<br />
Domain: <em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="_dmarc">_dmarc</em>
<br />
Value:
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ dmarc_record }}">
{{ dmarc_record }}
</em>
</div>
<div class="alert alert-info">
Some DNS registrar might require a full record path, in this case please use
<i>_dmarc.{{ custom_domain.domain }}</i> as domain value instead.
<br />
If you are using a subdomain, e.g. <i>subdomain.domain.com</i>,
you need to use <i>_dmarc.subdomain</i> as domain value instead.
<br />
</div>
<form method="post" action="#dmarc-form">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="check-dmarc">
{% if custom_domain.dmarc_verified %}
<button type="submit" class="btn btn-outline-primary">
Re-verify
</button>
{% else %}
<button type="submit" class="btn btn-primary">
Verify
</button>
{% endif %}
</form>
{% if not dmarc_ok %}
<div class="text-danger mt-4">
Your DNS is not correctly set.
The TXT record we obtain is:
<div class="mb-3 p-3" style="background-color: #eee">
{% if not dmarc_errors %}(Empty){% endif %}
{% for r in dmarc_errors %}
{{ r }}
<br />
{% endfor %}
</div>
{% if custom_domain.dmarc_verified %}
Without DMARC setup, emails sent from your alias might end up in the Spam/Junk folder.
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,179 @@
{% extends "dashboard/domain_detail/base.html" %}
{% set domain_detail_page = "info" %}
{% block title %}{{ custom_domain.domain }} Info{% endblock %}
{% block domain_detail_content %}
<h1 class="h2 mb-1">{{ custom_domain.domain }}</h1>
<div class="small-text">Created {{ custom_domain.created_at | dt }}. {{ nb_alias }} aliases</div>
<hr />
<h3 class="mb-1">Auto create/on the fly alias</h3>
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="switch-catch-all">
<label class="custom-switch cursor mt-2 pl-0" data-toggle="tooltip" {% if custom_domain.catch_all %}
title="Disable catch-all" {% else %} title="Enable catch-all" {% endif %}>
<input type="checkbox" class="custom-switch-input" {{ "checked" if custom_domain.catch_all else "" }}>
<span class="custom-switch-indicator"></span>
<spam class="ml-2">
Catch All
</spam>
</label>
</form>
<div>
Simply use <b>anything@{{ custom_domain.domain }}</b>
next time you need an alias: it'll be <b>automatically</b>
created the first time it receives an email.
To have more fine-grained control, you can also define
<a href="{{ url_for('dashboard.domain_detail_auto_create', custom_domain_id=custom_domain.id) }}">
auto create
rules
<i class="fe fe-chevrons-right"></i>
</a>
.
</div>
</div>
<div class="{% if not custom_domain.catch_all %} disabled-content{% endif %}">
<div>
Auto-created aliases are automatically owned by the following mailboxes
<i class="fe fe-corner-right-down"></i>
.
</div>
{% set domain_mailboxes=custom_domain.mailboxes %}
<form method="post" class="mt-2">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="update">
<input type="hidden" name="domain-id" value="{{ custom_domain.id }}">
<div class="d-flex">
<div class="flex-grow-1 mr-2">
<select data-width="100%"
required
class="mailbox-select"
multiple
name="mailbox_ids">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}"
{% if mailbox in domain_mailboxes %} selected{% endif %}>
{{ mailbox.email }}
</option>
{% endfor %}
</select>
</div>
<div>
<button class="btn btn-outline-primary btn-sm">Update</button>
</div>
</div>
</form>
</div>
<hr />
<h3 class="mb-1">Default Display Name</h3>
<div>
Default display name for aliases created with <b>{{ custom_domain.domain }}</b>
unless overwritten by the alias display name.
</div>
<div>
<form method="post" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="set-name">
<div class="form-group">
<input class="form-control mr-2"
value="{{ custom_domain.name or "" }}"
name="alias-name"
placeholder="Alias Display Name">
</div>
<button class="btn btn-outline-primary" name="action" value="save">Save</button>
{% if custom_domain.name %}
<button class="btn btn-outline-danger float-right ml-2"
name="action"
value="remove">Remove</button>
{% endif %}
</form>
</div>
<hr />
<h3 class="mb-1">Random Prefix Generation</h3>
<div>Add a random prefix for this domain when creating a new alias.</div>
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden"
name="form-name"
value="switch-random-prefix-generation">
<label class="custom-switch cursor mt-2 pl-0" data-toggle="tooltip" {% if custom_domain.random_prefix_generation %}
title="Disable random prefix generation" {% else %} title="Enable random prefix generation" {% endif %}>
<input type="checkbox" class="custom-switch-input" {{ "checked" if custom_domain.random_prefix_generation else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</form>
</div>
<hr />
<h3 class="mb-1">
{% if custom_domain.is_sl_subdomain %}
Delete Subdomain
{% else %}
Delete Domain
{% endif %}
</h3>
<div class="mb-3">
{% if custom_domain.is_sl_subdomain %}
<div class="alert alert-danger">
This operation is <b>irreversible</b>.
All aliases associated with this subdomain will be deleted.
</div>
<div class="alert alert-warning">
Because a deleted subdomain can't be recycled, i.e. reused by someone else,
deleting a subdomain won't restore the subdomain quota.
After deletion, your subdomain quota will still be {{ current_user.subdomain_quota }}.
We recommend to disable the <b>catch-all</b> option instead of deleting this subdomain.
</div>
{% else %}
<div class="alert alert-danger">
This operation is <b>irreversible</b>.
All aliases associated with this domain will be deleted.
</div>
{% endif %}
</div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete">
<span class="delete-custom-domain btn btn-danger">Delete {{ custom_domain.domain }}</span>
</form>
{% endblock %}
{% block script %}
<script>
$('.mailbox-select').multipleSelect();
$(".custom-switch-input").change(function (e) {
$(this).closest("form").submit();
});
$(".delete-custom-domain").on("click", function (e) {
let that = $(this);
bootbox.confirm({
message: "All aliases associated with <b>{{ custom_domain.domain }}</b> will be also deleted. <br />" +
"This operation is not reversible, please confirm.",
buttons: {
confirm: {
label: 'Yes, delete it',
className: 'btn-danger'
},
cancel: {
label: 'Cancel',
className: 'btn-outline-primary'
}
},
callback: function (result) {
if (result) {
that.closest("form").submit();
}
}
})
});
</script>
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "dashboard/domain_detail/base.html" %}
{% set domain_detail_page = "trash" %}
{% block title %}{{ custom_domain.domain }} deleted aliases{% endblock %}
{% block domain_detail_content %}
<h1 class="h3">
{{ custom_domain.domain }} deleted alias (aka Trash)
<a class="ml-3 text-info"
style="font-size: 12px"
data-toggle="collapse"
href="#howtouse"
role="button"
aria-expanded="false"
aria-controls="collapseExample">
How to use <i class="fe fe-chevrons-down"></i>
</a>
</h1>
<div class="alert alert-primary collapse" id="howtouse" role="alert">
On this page you can view all aliases that have been deleted and belong to the domain
<b>{{ custom_domain.domain }}</b>.
<br />
When an alias is in the trash, it cannot be re-created, either via the alias creation page or on-the-fly with the
domain catch-all option.
</div>
{% if domain_deleted_aliases | length > 0 %}
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="empty-all">
<button class="btn btn-outline-danger">Empty Trash</button>
<div class="small-text">
Remove all deleted aliases from the trash, allowing them to be re-created.
That operation is irreversible.
</div>
</form>
{% else %}
There's no deleted alias recorded for this domain.
{% endif %}
{% for deleted_alias in domain_deleted_aliases %}
<hr />
<b>{{ deleted_alias.email }}</b> -
deleted {{ deleted_alias.created_at | dt }}
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="remove-single">
<input type="hidden" name="deleted-alias-id" value="{{ deleted_alias.id }}">
<button class="btn btn-sm btn-outline-warning">Remove from trash</button>
</form>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,32 @@
{% extends "default.html" %}
{% set active_page = "setting" %}
{% block title %}SUDO MODE{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h2">Entering Sudo Mode</h1>
<p>The next page contains security related setting.</p>
<p>Please enter your account password so that we can ensure it's you.</p>
<form method="post">
{{ password_check_form.csrf_token }}
<div class="font-weight-bold mt-5">Password</div>
{{ password_check_form.password(class="form-control", autofocus="true") }}
{{ render_field_errors(password_check_form.password) }}
<button class="btn btn-lg btn-danger mt-2">Submit</button>
</form>
{% if connect_with_proton %}
<div class="my-3">
<p>Alternatively you can use your Proton credentials to ensure it's you.</p>
</div>
<a class="btn btn-primary btn-block mt-2 proton-button w-25"
href="{{ url_for("auth.proton_login", next=next) }}">
<img class="mr-2" src="/static/images/proton.svg" />
Authenticate with Proton
</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Extend Subscription{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h2">Extend Subscription</h1>
<p>Your subscription is expired on {{ coinbase_subscription.end_at.format("YYYY-MM-DD") }}</p>
<div>
<a class="buy-with-crypto"
data-custom="{{ current_user.id }}"
href="{{ coinbase_url }}">Extend for 1 year - $30</a>
<script src="https://commerce.coinbase.com/v1/checkout.js?version=201807"></script>
</div>
<div class="mt-2">
Your subscription will be extended when the payment is confirmed and we'll send you a confirmation email.
<br />
Please note that it can take up to 1h for processing a cryptocurrency payment.
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "default.html" %}
{% set active_page = "setting" %}
{% block title %}Manage Security Key{% endblock %}
{% block head %}
<script src="{{ url_for('static', filename='node_modules/qrious/dist/qrious.min.js') }}"></script>
{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h2">Manage Your Security Key</h1>
<p>Unlink all keys will also disable WebAuthn 2FA.</p>
<form id="formManageKey" method="post">
{{ fido_manage_form.csrf_token }}
{{ fido_manage_form.credential_id(class="form-control", placeholder="") }}
</form>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Linked At</th>
<th scope="col" class="text-center">Operation</th>
</tr>
</thead>
<tbody>
{% for key in keys %}
<tr>
<th scope="row">{{ key.id }}</th>
<td>{{ key.name }}</td>
<td>{{ key.created_at | dt }}</td>
<td class="text-center">
<button class="btn btn-outline-danger"
onclick="$('#credential_id').val('{{ key.credential_id }}'); $('#formManageKey').submit();">
Unlink
</button>
</td>
</tr>
{% endfor %}
<tr>
<th scope="row">#</th>
<td>Link a New Key</td>
<td></td>
<td class="text-center">
<a href="{{ url_for('dashboard.fido_setup') }}">
<button class="btn btn-outline-success">Link</button>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,73 @@
{% extends "default.html" %}
{% set active_page = "setting" %}
{% block title %}Security Key Setup{% endblock %}
{% block head %}
<script src="{{ url_for('static', filename='node_modules/qrious/dist/qrious.min.js') }}"></script>
<script src="{{ url_for('static', filename='assets/js/vendors/base64.js') }}"></script>
<script src="/static/assets/js/vendors/webauthn.js?v={{ VERSION }}"></script>
{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h2 text-center">Register Your Security Key</h1>
<p class="text-center">Follow your browser's steps to register your security key with SimpleLogin</p>
<form id="formRegisterKey" method="post">
{{ fido_token_form.csrf_token }}
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
{{ fido_token_form.key_name(id="key-name", class="form-control", placeholder="Name of your key (Required)") }}
{{ render_field_errors(fido_token_form.key_name) }}
<div class="text-center">
<span id="btnRegisterKey"
class="btn btn-lg btn-primary mt-2"
onclick="registerKey();">Register Key</span>
</div>
</form>
<script>
// disable submit when enter
$('form input').keydown(function (e) {
if (e.keyCode == 13) {
e.preventDefault();
return false;
}
});
async function registerKey() {
// make sure key name is not empty
if (!$("#key-name").val()) {
toastr.error("Key name cannot be empty.");
return
}
$("#btnRegisterKey").prop('disabled', true);
$("#btnRegisterKey").text('Waiting for Security Key...');
const pkCredentialCreateOptions = transformCredentialCreateOptions(
JSON.parse('{{credential_create_options|tojson|safe}}')
)
let credential
try {
credential = await navigator.credentials.create({
publicKey: pkCredentialCreateOptions
});
} catch (err) {
toastr.error("An error occurred when we trying to register your key.");
$("#btnRegisterKey").prop('disabled', false);
$("#btnRegisterKey").text('Register Key');
return console.error("Error when trying to create credential:", err);
}
const skAssertion = transformNewAssertionForServer(credential);
$('#sk_assertion').val(JSON.stringify(skAssertion));
$('#formRegisterKey').submit();
}
</script>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,559 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block head %}
<style>
.alias-activity {
font-weight: 600;
font-size: 14px;
}
.btn-group-border-left {
border-left: 1px #fbfbfb4f solid;
}
em {
padding: 0px;
}
{#CSS to change collapse button display from https://stackoverflow.com/a/31967516/1428034#}
[data-toggle="collapse"].collapsed .if-not-collapsed {
display: none;
}
[data-toggle="collapse"]:not(.collapsed) .if-collapsed {
display: none;
}
</style>
{% endblock %}
{% block title %}Alias{% endblock %}
{% block default_content %}
<!-- Global Stats -->
<div class="row">
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Aliases</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">All time</div>
</div>
<div class="h1 m-0">{{ stats.nb_alias }}</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Forwarded</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ stats.nb_forward }}</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Replies/Sent</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ stats.nb_reply }}</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="subheader">Blocked</div>
<div class="text-muted"
style="order: 2; margin-left: auto; font-size: .8rem">Last 14 days</div>
</div>
<div class="h1 m-0">{{ stats.nb_block }}</div>
</div>
</div>
</div>
</div>
<!-- END Global Stats -->
<!-- Controls: buttons & search -->
<div id="filter-app">
<div class="row mb-3">
<div class="col d-flex">
<div>
<div class="btn-group" role="group">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create-custom-email">
<button data-toggle="tooltip"
title="Create a custom alias"
class="btn btn-primary mr-2">
<i class="fa fa-plus"></i> New Custom Alias
</button>
</form>
<div class="btn-group" role="group">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create-random-email">
<button data-toggle="tooltip"
title="Create a totally random alias"
class="btn btn-success">
<i class="fa fa-random"></i> Random Alias
</button>
</form>
<button id="btnGroupDrop1"
type="button"
class="btn btn-success dropdown-toggle btn-group-border-left"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
</button>
<div class="dropdown-menu dropdown-menu-right border-left"
aria-labelledby="btnGroupDrop1">
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create-random-email">
<input type="hidden"
name="generator_scheme"
value="{{ AliasGeneratorEnum.word.value }}">
<button class="dropdown-item">By Random Words</button>
</form>
</div>
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="create-random-email">
<input type="hidden"
name="generator_scheme"
value="{{ AliasGeneratorEnum.uuid.value }}">
<button class="dropdown-item">By UUID</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div style="margin-left: auto">
<div class="btn-group">
<a v-if="!showFilter"
@click="toggleFilter()"
class="btn btn-outline-secondary">
<i class="fe fe-chevrons-down"></i> Filters
</a>
</div>
</div>
</div>
</div>
<div class="row mb-2" v-if="showFilter" id="filter-control">
<!-- Filter Control -->
<div class="col d-flex">
<form method="get" class="form-inline">
{{ csrf_form.csrf_token }}
<select name="sort"
onchange="this.form.submit()"
class="form-control mr-3 shadow">
<option value="" {% if sort == "" %} selected{% endif %}>
Sort by most recent activity
</option>
<option value="old2new" {% if sort == "old2new" %} selected{% endif %}>
Alias Old-Recent
</option>
<option value="new2old" {% if sort == "new2old" %} selected{% endif %}>
Alias Recent-Old
</option>
<option value="a2z" {% if sort == "a2z" %} selected{% endif %}>
Alias A-Z
</option>
<option value="z2a" {% if sort == "z2a" %} selected{% endif %}>
Alias Z-A
</option>
</select>
<select name="filter"
onchange="this.form.submit()"
class="form-control mr-3 shadow"
style="max-width: 200px">
<option value="" {% if filter == "" %} selected{% endif %}>
All Aliases
</option>
<option value="pinned" {% if filter == "pinned" %} selected{% endif %}>
Pinned Aliases
</option>
<option value="enabled" {% if filter == "enabled" %} selected{% endif %}>
Only Enabled Aliases
</option>
<option value="disabled" {% if filter == "disabled" %} selected{% endif %}>
Only Disabled Aliases
</option>
<option value="hibp" {% if filter == "hibp" %} selected{% endif %}>
Only Aliases Found In Data Breaches
</option>
{% for mailbox in current_user.mailboxes() %}
<option value="mailbox:{{ mailbox.id }}" {% if filter == "mailbox:" ~ mailbox.id %}
selected {% endif %}>
{{ mailbox.email }}'s aliases
</option>
{% endfor %}
{% for directory in current_user.directories %}
<option value="directory:{{ directory.id }}" {% if filter == "directory:" ~ directory.id %}
selected {% endif %}>
Directory <b>{{ directory.name }}</b> aliases
</option>
{% endfor %}
</select>
<input type="search"
name="query"
placeholder="Enter to search for alias"
class="form-control shadow mr-2"
style="max-width: 15em"
value="{{ query }}">
</form>
<div style="margin-left: auto">
{% if query or sort or filter %}
<a href="{{ url_for('dashboard.index') }}"
class="btn btn-outline-secondary">Reset</a>
{% endif %}
<a v-if="showFilter"
@click="toggleFilter()"
class="btn btn-outline-secondary">
<i class="fe fe-chevrons-up"></i>
</a>
</div>
</div>
</div>
</div>
<!-- END Controls: buttons & search -->
<!-- Alias list -->
<div class="row">
{% for alias_info in alias_infos %}
{% set alias = alias_info.alias %}
<div class="col-12 col-lg-6" id="alias-container-{{ alias.id }}">
<div class="card p-4 shadow-sm {% if alias.id == highlight_alias_id %} highlight-row{% endif %} ">
<div class="row">
<div class="col-8">
<span class="{% if alias.id == highlight_alias_id %} highlighted{% endif %} clipboard cursor mb-0" {% if loop.index ==1 %}
data-intro="This is your first <em>alias</em>.
<br />
<br />
Emails sent to an alias are <em>forwarded</em> to your <em>real</em> email address.
<br />
<br />
" data-step="2"
{% endif %}
{% if alias.enabled %}data-toggle="tooltip" title="Click to copy" data-clipboard-text="{{ alias.email }}"{% endif %}
>
<span class="font-weight-bold">{{ alias.email }}</span>
</span>
{% if alias.automatic_creation %}
<span class="fa fa-inbox"
data-toggle="tooltip"
title="This alias was automatically generated because of an incoming email"></span>
{% endif %}
{% if alias.pinned %}
<span class="fa fa-thumb-tack"
data-toggle="tooltip"
title="This alias is pinned"></span>
{% endif %}
{% if alias.hibp_breaches | length > 0 %}
<a href="https://haveibeenpwned.com/account/{{ alias.email }}">
<span class="fa fa-warning text-danger"
data-toggle="tooltip"
title="This alias was found in {{ alias.hibp_breaches | length }} data breaches. Check haveibeenpwned.com for more information."></span>
</a>
{% endif %}
{% if alias.custom_domain and not alias.custom_domain.verified %}
<span class="fa fa-warning text-warning"
data-toggle="tooltip"
title="Alias can't receive emails as its domain doesn't have MX records set up."></span>
{% endif %}
</div>
<div class="col text-right">
<label class="custom-switch cursor" data-toggle="tooltip" {% if alias.enabled %}
title="Disable alias - stop receiving emails sent to this alias" {% else %} title="Enable alias - start receiving emails sent to this alias" {% endif %} {% if loop.index ==1 %}
data-intro="By <em>disabling</em> an alias, emails sent to this alias will <em>not</em> be forwarded to your inbox.
<br />
<br />
" data-step="3"
{% endif %}
style="padding-left: 0px">
<input type="checkbox" class="enable-disable-alias custom-switch-input" data-alias="{{ alias.id }}" data-alias-email="{{ alias.email }}" {{ "checked" if alias.enabled else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</div>
</div>
<!-- Email Activity -->
<div class="row mb-2">
<div class="col">
<div style="font-size: 12px">
{% if alias_info.latest_email_log != None %}
{% set email_log = alias_info.latest_email_log %}
{% set contact = alias_info.latest_contact %}
{% if email_log.is_reply %}
{{ contact.website_email }}
<i class="fa fa-reply mr-2"
data-toggle="tooltip"
title="Email reply/sent from alias"></i>
{{ email_log.created_at | dt }}
{% elif email_log.bounced %}
<span class="text-danger">
{{ contact.website_email }}
<i class="fa fa-warning mr-2"
data-toggle="tooltip"
title="Email bounced and cannot be forwarded to your mailbox"></i>
{{ email_log.created_at | dt }}
</span>
{% elif email_log.blocked %}
{{ contact.website_email }}
<i class="fa fa-ban mr-2 text-danger"
data-toggle="tooltip"
title="Email blocked"></i>
{{ email_log.created_at | dt }}
{% else %}
{{ contact.website_email }}
<i class="fa fa-paper-plane mr-2"
data-toggle="tooltip"
title="Email sent to alias"></i>
{{ email_log.created_at | dt }}
{% include 'partials/toggle_contact.html' %}
{% endif %}
{% else %}
No emails received/sent in the last 14 days. Created {{ alias.created_at | dt }}.
{% endif %}
</div>
</div>
</div>
<!-- END Email Activity -->
<div class="small-text mt-1">
Alias description
</div>
<div class="d-flex mb-2">
<div class="flex-grow-1 mr-2">
<textarea id="note-{{ alias.id }}" name="note" class="form-control" style="font-size: 12px" rows="2" placeholder="e.g. where the alias is used or why is it created">{{ alias.note or "" }}</textarea>
</div>
<div>
<a data-alias="{{ alias.id }}"
class="save-note btn btn-sm btn-outline-success w-100">
Save
</a>
</div>
</div>
<!-- Send Email && More button -->
<div class="row">
<div class="col">
<a href="{{ url_for('dashboard.alias_contact_manager', alias_id=alias.id) }}" id="send-email-{{ alias.id }}" {% if loop.index ==1 %}
data-intro="Not only can an alias receive emails, it can <em>send</em> emails too!
<br />
<br />
You can add a new <em>contact</em> for your alias here.
<br />
<br />
If you need to reply to an email, just hit 'Reply': the response will come from your alias and your real email address stays <em>hidden</em>." data-step="4"
{% endif %}
class="btn btn-sm btn-outline-primary
{% if not alias.enabled %}disabled{% endif %}
" data-toggle="tooltip" title="Add new contact, manage your existing contacts">
Contacts&nbsp; &nbsp;<i class="fe fe-send"></i>
</a>
</div>
{% if not current_user.expand_alias_info %}
<div class="col text-right">
<a class="btn btn-sm collapsed"
data-toggle="collapse"
href="#alias-{{ alias.id }}"
role="button"
aria-expanded="false">
<span class="if-collapsed">
More <i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">Less
<i class="fe fe-chevron-up"></i>
</span>
</a>
</div>
{% endif %}
</div>
<!-- END Send Email && More button -->
<!-- Collapse section -->
<div class="{% if not current_user.expand_alias_info %} collapse{% endif %} mt-2"
id="alias-{{ alias.id }}">
{% if alias_info.latest_email_log != None %}
<div style="font-size: 12px">
Alias created {{ alias.created_at | dt }}
</div>
{% endif %}
<span class="alias-activity">{{ alias_info.nb_forward }}</span> forwarded,
<span class="alias-activity">{{ alias_info.nb_blocked }}</span> blocked,
<span class="alias-activity">{{ alias_info.nb_reply }}</span> sent
in the last 14 days
<a href="{{ url_for('dashboard.alias_log', alias_id=alias.id) }}"
class="btn btn-sm btn-link">
See All &nbsp;
</a>
{% if mailboxes|length > 1 %}
<div class="small-text">
Current mailbox
</div>
<div class="d-flex">
<div class="flex-grow-1 mr-2">
<select required
id="mailbox-{{ alias.id }}"
data-width="100%"
class="mailbox-select"
multiple
name="mailbox">
{% for mailbox in mailboxes %}
<option value="{{ mailbox.id }}" {% if alias_info.contain_mailbox(mailbox.id) %}
selected {% endif %}>
{{ mailbox.email }}
</option>
{% endfor %}
</select>
</div>
<div>
<a data-alias="{{ alias.id }}"
class="save-mailbox btn btn-sm btn-outline-info w-100">
Update
</a>
</div>
</div>
{% elif alias_info.mailbox != None and alias_info.mailbox.email != current_user.email %}
<div class="small-text">
Owned by <b>{{ alias_info.mailbox.email }}</b> mailbox
</div>
{% endif %}
<div class="small-text mt-2"
data-toogle="tooltip"
title="When sending an email from this alias, the email will have 'Display Name <{{ alias.email }}>' as sender.">
Display name
<i class="fe fe-help-circle"></i>
</div>
<div class="d-flex">
<div class="flex-grow-1 mr-2">
<input id="alias-name-{{ alias.id }}"
value="{{ alias.name or '' }}"
class="form-control"
placeholder="{{ alias.custom_domain.name or "Alias name" }}">
</div>
<div>
<a data-alias="{{ alias.id }}"
class="save-alias-name btn btn-sm btn-outline-primary w-100">
Save
</a>
</div>
</div>
{% if alias.mailbox_support_pgp() %}
<div class="small-text mt-2"
data-toogle="tooltip"
title="You can decide to turn off the PGP for an alias. This can be useful if the sender already encrypts the emails">
PGP
<i class="fe fe-help-circle"></i>
</div>
<div>
<label class="custom-switch cursor pl-0">
<input type="checkbox" class="enable-disable-pgp custom-switch-input" data-alias="{{ alias.id }}" data-alias-email="{{ alias.email }}" {{ "checked" if alias.pgp_enabled() else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</div>
{% endif %}
<div class="small-text mt-2"
data-toogle="tooltip"
title="it's always pinned on top">
Pin this alias
<i class="fe fe-help-circle"></i>
</div>
<div>
<label class="custom-switch cursor pl-0">
<input type="checkbox" class="pin-alias custom-switch-input" data-alias="{{ alias.id }}" data-alias-email="{{ alias.email }}" {{ "checked" if alias.pinned else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</div>
<div>
<div class="btn-group float-right" role="group" aria-label="Basic example">
<a href="{{ url_for('dashboard.alias_transfer_send_route', alias_id=alias.id) }}"
class="btn btn-sm btn-link">
Transfer
<i class="ml-0 dropdown-icon fe fe-share-2 text-primary"></i>
</a>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete-alias">
<input type="hidden" name="alias-id" value="{{ alias.id }}">
<input type="hidden" name="alias" class="alias" value="{{ alias.email }}">
<span class="btn btn-link btn-sm float-right text-danger"
onclick="confirmDeleteAlias.call(this)"
{% if alias.custom_domain %} data-custom-domain-trash-url="{{ alias.custom_domain.get_trash_url() }}"{% endif %}
data-alias="{{ alias.id }}"
data-alias-email="{{ alias.email }}">
Delete&nbsp; &nbsp;<i class="dropdown-icon fe fe-trash-2 text-danger"></i>
</span>
</form>
</div>
</div>
</div>
<!-- END Collapse section -->
</div>
</div>
{% endfor %}
</div>
<!-- END Alias list -->
<!-- Only show pagination control if there are previous/next page -->
{% if page > 0 or not last_page %}
<div class="row">
<div class="col">
<nav aria-label="Alias navigation">
<ul class="pagination">
<li class="page-item mr-1">
<a class="btn btn-outline-primary {% if page == 0 %}disabled{% endif %}"
href="{{ url_for('dashboard.index', page=page-1, query=query, sort=sort, filter=filter) }}">
Previous
</a>
</li>
<li class="page-item">
<a class="btn btn-outline-primary {% if last_page %}disabled{% endif %}"
href="{{ url_for('dashboard.index', page=page+1, query=query, sort=sort, filter=filter) }}">
Next
</a>
</li>
</ul>
</nav>
</div>
</div>
{% endif %}
{% endblock %}
{% block script %}
<script src="/static/js/index.js?v=0"></script>
<script>
{% if show_intro %}
// only show intro when screen is big enough to show "developer" tab
if (window.innerWidth >= 1024) {
introJs().start();
}
{% endif %}
$('.highlighted').tooltip("show");
</script>
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Lifetime Licence{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h2">Lifetime Licence</h1>
<div class="mb-4">
If you have a lifetime licence, please paste it here.
<br />
</div>
<form method="post">
{{ coupon_form.csrf_token }}
{{ coupon_form.code(class="form-control", placeholder="Licence Code") }}
{{ render_field_errors(coupon_form.code) }}
<button class="btn btn-success mt-2">Apply</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,158 @@
{% extends "default.html" %}
{% set active_page = "mailbox" %}
{% block title %}Mailboxes{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">
Mailboxes
<a class="ml-3 text-info"
style="font-size: 12px"
data-toggle="collapse"
href="#howtouse"
role="button"
aria-expanded="false"
aria-controls="collapseExample">
How to use <i class="fe fe-chevrons-down"></i>
</a>
</h1>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
{% endif %}
<div class="alert alert-primary collapse {% if mailboxes|length == 1 %} show{% endif %}"
id="howtouse"
role="alert">
A <em>mailbox</em> is just another personal email address. When creating a new alias, you could choose the
mailbox that <em>owns</em> this alias, i.e:
<br />
- all emails sent to this alias will be forwarded to this mailbox
<br />
- from this mailbox, you can reply/send emails from the alias.
<br />
<br />
When you signed up, a mailbox is automatically created with your email <b>{{ current_user.email }}</b>
<br />
<br />
The mailbox doesn't have to be your email: it can be your friend's email
if you want to create aliases for your buddy.
</div>
<div class="row">
{% for mailbox in mailboxes %}
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">
{{ mailbox.email }}
{% if mailbox.verified %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="Mailbox Verified"></span>
{% else %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="Mailbox Not Verified">🚫</span>
{% endif %}
{% if mailbox.pgp_enabled() %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="PGP Enabled">🗝</span>
{% endif %}
{% if mailbox.id == current_user.default_mailbox_id %}
<div class="badge badge-primary float-right"
data-toggle="tooltip"
title="When a new random alias is created, it belongs to the default mailbox">
Default Mailbox
</div>
{% endif %}
</h5>
<h6 class="card-subtitle mb-2 text-muted">
Created {{ mailbox.created_at | dt }}
<br />
<span class="font-weight-bold">{{ mailbox.nb_alias() }}</span> aliases.
<br />
</h6>
<a href="{{ url_for('dashboard.mailbox_detail_route', mailbox_id=mailbox.id) }}">Edit ➡</a>
</div>
<div class="card-footer p-0">
<div class="row">
{% if mailbox.verified %}
<div class="col">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="set-default">
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
<button class="card-link btn btn-link {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
Set As Default Mailbox
</button>
</form>
</div>
{% endif %}
<div class="col">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete">
<input type="hidden" class="mailbox" value="{{ mailbox.email }}">
<input type="hidden" name="mailbox-id" value="{{ mailbox.id }}">
<span class="card-link btn btn-link text-danger float-right delete-mailbox {% if mailbox.id == current_user.default_mailbox_id %} disabled{% endif %}">
Delete
</span>
</form>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<form method="post" class="mt-2">
{{ new_mailbox_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<h2 class="h4">New Mailbox</h2>
{{ new_mailbox_form.email(class="form-control", placeholder="email@example.com") }}
{{ render_field_errors(new_mailbox_form.email) }}
<div class="small-text">A mailbox can't be a disposable or forwarding email address.</div>
<button class="btn btn-primary mt-2">Create</button>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-mailbox").on("click", function (e) {
let mailbox = $(this).parent().find(".mailbox").val();
let that = $(this);
let message = `All aliases owned by this mailbox <b>${mailbox}</b> will be also deleted, ` +
" please confirm.";
bootbox.confirm({
message: message,
buttons: {
confirm: {
label: 'Yes, delete it',
className: 'btn-danger'
},
cancel: {
label: 'Cancel',
className: 'btn-outline-primary'
}
},
callback: function (result) {
if (result) {
that.closest("form").submit();
}
}
})
});
</script>
{% endblock %}

View File

@ -0,0 +1,247 @@
{% extends "default.html" %}
{% set active_page = "mailbox" %}
{% block title %}Mailbox {{ mailbox.email }}{% endblock %}
{% block head %}
<style>
div[disabled]
{
pointer-events: none;
opacity: 0.7;
}
</style>
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">
{{ mailbox.email }}
{% if mailbox.verified %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="Mailbox Verified"></span>
{% else %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="Mailbox Not Verified">🚫</span>
{% endif %}
{% if mailbox.pgp_enabled() %}
<span class="cursor"
data-toggle="tooltip"
data-original-title="PGP Enabled">🗝</span>
{% endif %}
</h1>
{% if not mailbox.verified %}
<div class="alert alert-info">
Mailbox not verified, please check your inbox/spam folder for the verification email.
<br />
To receive the verification email again, you can delete and re-add the mailbox.
</div>
{% endif %}
<!-- Change email -->
<div class="card">
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="form-name" value="update-email">
{{ change_email_form.csrf_token }}
<div class="card-body">
<div class="card-title">Change Mailbox Address</div>
<div class="form-group">
<label class="form-label">Address</label>
<!-- Not allow user to change mailbox if there's a pending change -->
{{ change_email_form.email(class="form-control", value=mailbox.email, readonly=pending_email != None) }}
{{ render_field_errors(change_email_form.email) }}
{% if pending_email %}
<div class="mt-2">
<span class="text-danger">Pending change: {{ pending_email }}</span>
<a href="{{ url_for('dashboard.cancel_mailbox_change_route', mailbox_id=mailbox.id) }}"
class="btn btn-secondary btn-sm">
Cancel mailbox change
</a>
</div>
{% endif %}
</div>
<button class="btn btn-primary">Change</button>
</div>
</form>
</div>
<!-- END Change email -->
{% if mailbox.pgp_finger_print and not mailbox.disable_pgp and current_user.include_sender_in_reverse_alias %}
<div class="alert alert-info">
Email headers like <span class="italic">From, To, Subject</span> aren't encrypted by PGP.
Currently, your reverse alias includes the sender address.
You can disable this on <a href="/dashboard/setting#sender-in-ra">Settings</a>.
</div>
{% endif %}
<div class="card">
<div class="card-body">
<div class="card-title">
<div class="d-flex">
Pretty Good Privacy (PGP)
{% if mailbox.pgp_finger_print %}
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="toggle-pgp">
<label class="custom-switch cursor" style="padding-left: 1rem" data-toggle="tooltip" {% if mailbox.disable_pgp %}
title="Enable PGP" {% else %} title="Disable PGP" {% endif %}>
<input type="checkbox" class="custom-switch-input" name="pgp-enabled" {{ "" if mailbox.disable_pgp else "checked" }}>
<span class="custom-switch-indicator"></span>
</label>
</form>
{% endif %}
</div>
<div class="small-text mt-1">
By importing your PGP Public Key into SimpleLogin, all emails sent to {{ mailbox.email }} are
<b>encrypted</b> with your key.
<br />
{% if PGP_SIGNER %}All forwarded emails will be signed with <b>{{ PGP_SIGNER }}</b>.{% endif %}
</div>
</div>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">This feature is only available in premium plan.</div>
{% endif %}
<form method="post">
{{ csrf_form.csrf_token }}
<div class="form-group">
<label class="form-label">PGP Public Key</label>
<textarea name="pgp" {% if not current_user.is_premium() %} disabled {% endif %} class="form-control" rows=10 id="pgp-public-key" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----">{{ mailbox.pgp_public_key or "" }}</textarea>
</div>
<input type="hidden" name="form-name" value="pgp">
<button class="btn btn-primary" name="action" {% if not current_user.is_premium() %}
disabled {% endif %} value="save">
Save
</button>
{% if mailbox.pgp_finger_print %}
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
{% endif %}
</form>
</div>
</div>
<div class="card" {% if not mailbox.pgp_enabled() %}
disabled {% endif %}>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="generic-subject">
<div class="card-body">
<div class="card-title">
Hide email subject when PGP is enabled
<div class="small-text mt-1">
When PGP is enabled, you can choose to use a <b>generic</b> subject for the forwarded emails.
The original subject is then added into the email body.
<br />
As PGP does not encrypt the email subject and the email subject might contain sensitive information,
this option will allow a further protection of your email content.
</div>
</div>
<div class="alert alert-info">
As the email is encrypted, a subject like "Email for you"
will probably be rejected by your mailbox since it sounds like a spam.
<br />
Something like "Encrypted Email" would work much better :).
</div>
<div class="form-group">
<label class="form-label">Generic Subject</label>
<input name="generic-subject" {% if not mailbox.pgp_enabled() %}
disabled {% endif %} class="form-control" maxlength="78" placeholder="Generic Subject" value="{{ mailbox.generic_subject or "" }}">
</div>
<button class="btn btn-primary" name="action" {% if not mailbox.pgp_enabled() %}
disabled {% endif %} value="save">
Save
</button>
{% if mailbox.generic_subject %}
<button class="btn btn-danger float-right" name="action" value="remove">Remove</button>
{% endif %}
</div>
</form>
</div>
<hr />
<h2 class="h4">Advanced Options</h2>
{% if spf_available %}
<div class="card" id="spf">
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="force-spf">
<div class="card-body">
<div class="card-title">
Enforce SPF
<div class="small-text">
To avoid email-spoofing, SimpleLogin blocks email that
<em data-toggle="tooltip"
title="Email that has your mailbox as envelope-sender address">seems</em> to come from your
mailbox
but sent from <em data-toggle="tooltip"
title="IP Address that is not known by your mailbox email service">unknown</em>
IP address.
<br />
Only turn off this option if you know what you're doing :).
</div>
</div>
<label class="custom-switch cursor mt-2 pl-0" data-toggle="tooltip" {% if mailbox.force_spf %}
title="Disable SPF enforcement" {% else %} title="Enable SPF enforcement" {% endif %}>
<input type="checkbox" name="spf-status" class="custom-switch-input" {{ "checked" if mailbox.force_spf else "" }}>
<span class="custom-switch-indicator"></span>
</label>
</div>
</form>
</div>
{% endif %}
<div class="card" id="authorized-address">
<div class="card-body">
<div class="card-title">
Authorized addresses
<div class="small-text">
Emails sent from these addresses to a <b>reverse-alias</b> are considered as being sent
from {{ mailbox.email }}
</div>
</div>
{% if mailbox.authorized_addresses | length == 0 %}
{% else %}
<ul>
{% for authorized_address in mailbox.authorized_addresses %}
<li>
{{ authorized_address.email }}
<form method="post" action="#authorized-address" style="display: inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="delete-authorized-address">
<input type="hidden"
name="authorized-address-id"
value="{{ authorized_address.id }}">
<input type="submit" class="btn btn-sm btn-outline-warning" value="Delete">
</form>
</li>
{% endfor %}
</ul>
{% endif %}
<form method="post" action="#authorized-address" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="add-authorized-address">
<input type="email" name="email" size="50" class="form-control" required>
<input type="submit" class="btn btn-primary" value="Add">
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script src="/static/js/utils/drag-drop-into-text.js"></script>
<script>
$(".custom-switch-input").change(function (e) {
$(this).closest("form").submit();
});
enableDragDropForPGPKeys('#pgp-public-key');
</script>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}Mailbox Validation{% endblock %}
{% block content %}
<div class="mx-auto p-7 card" style="max-width: 50rem">
<div class="text-center mb-7">
Mailbox <b>{{ mailbox.email }}</b> verified, you can now start creating alias with it
</div>
<div class="mx-auto">
<a href="{{ url_for('dashboard.index') }}" class="btn btn-primary">Go To Home Page</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "default.html" %}
{% set active_page = "setting" %}
{% block title %}Cancel MFA{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h2">Two Factor Authentication</h1>
<div>
Disabling TOTP reduces the security of your account, please make sure to re-activate it later
or use WebAuthn (FIDO).
</div>
<form method="post">
<button class="btn btn-danger mt-2">Disable TOTP</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "default.html" %}
{% set active_page = "setting" %}
{% block title %}MFA Setup{% endblock %}
{% block head %}
<script src="{{ url_for('static', filename='node_modules/qrious/dist/qrious.min.js') }}"></script>
{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">Two Factor Authentication - TOTP</h1>
<p>
You will need to use a 2FA application like Google Authenticator or Authy on your phone or PC and scan the following QR Code:
</p>
<canvas id="qr"></canvas>
<script>
(function () {
var qr = new QRious({
element: document.getElementById('qr'),
value: '{{otp_uri}}'
});
})();
</script>
<div class="mt-3 mb-2">Or you can manually enter the following secret key:</div>
<input class="form-control" disabled value="{{ current_user.otp_secret }}">
<form method="post">
{{ otp_token_form.csrf_token }}
<div class="font-weight-bold mt-5">Token</div>
<div class="small-text">Please enter the 6-digit number displayed in your authenticator app.</div>
{{ otp_token_form.token(class="form-control", placeholder="") }}
{{ render_field_errors(otp_token_form.token) }}
<button class="btn btn-lg btn-success mt-2">Submit</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends "default.html" %}
{% block title %}API Key{% endblock %}
{% set active_page = "api_key" %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">New API Key {{ api_key.name }} is created</h1>
<div class="alert alert-warning">For security reasons, API Key is only visible when it is created.</div>
<div class="input-group mb-2">
<input class="form-control"
id="apikey-{{ api_key.id }}"
readonly
value="**********">
<div class="input-group-append">
<span class="input-group-text">
<i class="fe fe-eye toggle-api-key"
data-show="off"
data-secret="{{ api_key.code }}"></i>
</span>
</div>
</div>
<button class="clipboard btn btn-primary"
data-clipboard-action="copy"
data-clipboard-text="{{ api_key.code }}"
data-clipboard-target="#apikey-{{ api_key.id }}">
Copy &nbsp; &nbsp; <i class="fe fe-clipboard"></i>
</button>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".toggle-api-key").on('click', function (event) {
let that = $(this);
let apiInput = that.parent().parent().parent().find("input");
if (that.attr("data-show") === "off") {
let apiKey = $(this).attr("data-secret");
apiInput.val(apiKey);
that.addClass("fe-eye-off");
that.removeClass("fe-eye");
that.attr("data-show", "on");
} else {
that.removeClass("fe-eye-off");
that.addClass("fe-eye");
apiInput.val("**********");
that.attr("data-show", "off");
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}{{ notification.title }}{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">{{ notification.title or "" }}</h1>
<div>{{ notification.message | safe }}</div>
<form method="post"
class="float-right mt-3"
onsubmit="return confirm('This operation is not reversible, please confirm');">
<button class="btn btn-outline-danger">Delete</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Notifications{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">Notifications</h1>
{% for notification in notifications %}
<div class="card">
<div class="card-body">
<div class="h4">{{ notification.title or "" }}</div>
<div style="width: 40em;
word-wrap:break-word;
white-space: normal;
overflow: hidden;
max-height: 100px;
text-overflow: ellipsis;">
{{ notification.message | safe }}
</div>
<a href="{{ url_for('dashboard.notification_route', notification_id=notification.id) }}"
class="mt-2 btn btn-outline-primary">
More ➡
</a>
<div class="small text-muted mt-2">{{ notification.created_at | dt }}</div>
</div>
</div>
{% endfor %}
<!-- Only show pagination control if there are previous/next page -->
{% if page > 0 or not last_page %}
<div class="row">
<div class="col">
<nav aria-label="Notification navigation">
<ul class="pagination">
<li class="page-item mr-1">
<a class="btn btn-outline-primary {% if page == 0 %}disabled{% endif %}"
href="{{ url_for('dashboard.notifications_route', page=page-1) }}">
Previous
</a>
</li>
<li class="page-item">
<a class="btn btn-outline-primary {% if last_page %}disabled{% endif %}"
href="{{ url_for('dashboard.notifications_route', page=page+1) }}">
Next
</a>
</li>
</ul>
</nav>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,218 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Pricing{% endblock %}
{% block head %}
<script src="https://cdn.paddle.com/paddle/paddle.js"></script>
<script>
if (window.Paddle === undefined) {
console.log("cannot load Paddle from CDN");
document.write('<script src="/static/vendor/paddle.js"><\/script>')
}
</script>
<style type="text/css">
html.mvc__a.mvc__lot.mvc__of.mvc__classes.mvc__to.mvc__increase.mvc__the.mvc__odds.mvc__of.mvc__winning.mvc__specificity, html.mvc__a.mvc__lot.mvc__of.mvc__classes.mvc__to.mvc__increase.mvc__the.mvc__odds.mvc__of.mvc__winning.mvc__specificity > body {
position: static;
}
{#CSS to change collapse button display from https://stackoverflow.com/a/31967516/1428034#}
[data-toggle="collapse"].collapsed .if-not-collapsed {
display: none;
}
[data-toggle="collapse"]:not(.collapsed) .if-collapsed {
display: none;
}
</style>
{% endblock %}
{% block announcement %}
{# TODO: to remove#}
{# <div class="alert alert-danger text-center mb-0" role="alert">#}
{# Our payment provider Paddle is experiencing#}
{# <a href="https://paddle.status.io" target="_blank">server issue <i class="fe fe-external-link"></i></a>#}
{# that can make our checkout page unusable. <br />#}
{# Please retry later and sorry for this issue!#}
{# </div>#}
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col-sm-6 col-lg-6">
<div class="card">
<div class="card-body text-center">
<div class="h3">Premium</div>
<ul class="list-unstyled leading-loose mb-3">
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Unlimited aliases
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Unlimited custom domains
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Catch-all (or wildcard) aliases
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Up to 50 directories (or usernames)
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
Unlimited mailboxes
</li>
<li>
<i class="fe fe-check text-success mr-2" aria-hidden="true"></i>
PGP Encryption
</li>
</ul>
<div class="small-text">
More information on our
<a href="https://simplelogin.io/pricing" target="_blank" rel="noopener">
Pricing
Page <i class="fe fe-external-link"></i>
</a>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-6">
{% if manual_sub %}
<div class="alert alert-info">
You currently have a subscription until <b>{{ manual_sub.end_at.format("YYYY-MM-DD") }}</b>
({{ (manual_sub.end_at - now).days }} days left).
<br />
Please note that the time left will <b>not</b> be taken into account in a new subscription.
</div>
<hr />
{% endif %}
{% if proton_upgrade %}
<div id="proton-upgrade">
<h4>Proton Unlimited, Business and Visionary plans include SimpleLogin premium and more!</h4>
<a class="btn btn-primary" role="button" href="https://account.proton.me/u/0/mail/upgrade">
<b>Upgrade your Proton account</b>
</a>
<p class="mt-2 small">
Starts at $9.99/month (billed yearly), starting with 500GB of storage, VPN, encrypted
calendar & file storage and more.
</p>
<div class="middle-line my-5 h4">OR</div>
<div id="normal-upgrade-button">
<a class="btn btn-secondary collapsed" data-toggle="collapse" href="#normal-upgrade" role="button">
Upgrade your SimpleLogin account
<span class="if-collapsed">
<i class="fe fe-chevron-down"></i>
</span>
<span class="if-not-collapsed">
<i class="fe fe-chevron-up"></i>
</span>
</a>
<p class="mt-2 small">Starts at $2.5/month (billed yearly)</p>
</div>
</div>
{% endif %}
<div id="normal-upgrade" class="{% if proton_upgrade %} collapse{% endif %}">
<div class="display-6 my-3">
🔐 Secure payments by
<a href="https://paddle.com" target="_blank" rel="noopener">
Paddle <i class="fe fe-external-link"></i>
</a>
</div>
{% set sub = current_user.get_paddle_subscription() %}
{% if sub and sub.cancelled %}
<div class="alert alert-primary" role="alert">
You have an active subscription until {{ sub.next_bill_date.strftime("%Y-%m-%d") }}.
<br />
Please note that if you re-subscribe now, this will be a completely
new subscription and
your payment method will be charged <b>immediately</b>.
</div>
{% endif %}
{% if coinbase_sub %}
<div class="alert alert-info">
You currently have a Coinbase subscription until <b>{{ coinbase_sub.end_at.format("YYYY-MM-DD") }}</b>
({{ (coinbase_sub.end_at - now).days }} days left).
<br />
Please note that the time left will <b>not</b> be taken into account in a new Paddle subscription.
</div>
{% endif %}
<div class="mb-3">
Paddle supports bank cards
(Mastercard, Visa, American Express, etc) and PayPal.
</div>
<button class="btn btn-primary" onclick="upgrade({{ PADDLE_YEARLY_PRODUCT_ID }})">
Yearly billing
<span class="badge badge-success">Save $18</span>
<br />
<span style="font-size: 18px">$30/year</span>
</button>
<button class="btn btn-secondary" onclick="upgrade({{ PADDLE_MONTHLY_PRODUCT_ID }})">
Monthly billing
<br />
<b>
$4/month
</b>
</button>
<hr />
<i class="fa fa-bitcoin"></i>
Payment via
<a href="https://commerce.coinbase.com/?lang=en" target="_blank">
Coinbase Commerce<i class="fe fe-external-link"></i>
</a>
<br />
Currently Bitcoin, Bitcoin Cash, Dai, Ethereum, Litecoin and USD Coin are supported.
<br />
<a class="btn btn-outline-primary" href="{{ url_for('dashboard.coinbase_checkout_route') }}" target="_blank">
Yearly billing - Crypto
<br />
$30/year
<i class="fe fe-external-link"></i>
</a>
<hr />
For other payment options, please send us an email at
<a href="mailto:hi@simplelogin.io">hi@simplelogin.io</a>
.
<br />
If you have bought a coupon, please go to the
<a href="{{ url_for('dashboard.coupon_route') }}">coupon page</a>
to apply the coupon code.
</div>
</div>
</div>
<script type="text/javascript">
Paddle.Setup({vendor: {{ PADDLE_VENDOR_ID }}});
function upgrade(productId) {
bootbox.dialog({
title: `Payment with credit card or PayPal via Paddle`,
message: `Paddle will ask for an email address for sending out the invoices, please feel free to use an alias. <br />
You don't have to use your SimpleLogin account email address`,
size: 'large',
onEscape: true,
backdrop: true,
buttons: {
got_it: {
label: 'Got it!',
className: 'btn-outline-primary',
callback: function () {
Paddle.Checkout.open({
product: productId,
success: "{{ success_url }}",
passthrough: "{\"user_id\": {{current_user.id}} }"
});
}
},
}
});
}
</script>
{% endblock %}

View File

@ -0,0 +1,26 @@
{% extends "default.html" %}
{% set active_page = "setting" %}
{% block title %}Recovery Codes{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">Recovery codes</h1>
<p>
In the event you lose access to your authenticator app you can use one of these recovery codes to gain access to
your account again.
Each code can only be used once, make sure to store them in a safe place.
</p>
<p class="alert alert-warning">
<strong>
If you had recovery codes before, they have been invalidated.
Store these codes in a safe place. You won't be able to retrieve them again!
</strong>
</p>
<ul>
{% for recovery_code in recovery_codes %}<li>{{ recovery_code }}</li>{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,197 @@
{% extends "default.html" %}
{% block title %}Referral{% endblock %}
{% set active_page = "setting" %}
{% block default_content %}
<div class="col">
<h1 class="h3 mb-5">
Referrals
<a class="ml-3 text-info"
style="font-size: 12px"
data-toggle="collapse"
href="#howtouse"
role="button"
aria-expanded="false"
aria-controls="collapseExample">
How to use <i class="fe fe-chevrons-down"></i>
</a>
</h1>
<div class="alert alert-primary collapse" id="howtouse" role="alert">
On this page, you can create a <b>referral code</b> that you can use when referring people to SimpleLogin.
For every user who <b>upgrades</b> and stays with us at least 3 months, you'll get $5 :).
<br />
The payout can be initiated any time, just send us an email at
<a href="mailto:hi@simplelogin.io">hi@simplelogin.io</a>
when you want to receive the payout.
</div>
{% if referrals|length == 0 %}
<div class="alert alert-info">
You don't have any referral code yet. Let's create the first one and start inviting your friends!
<br />
</div>
{% endif %}
{% for referral in referrals %}
<div class="card p-4 shadow-sm {% if referral.id == highlight_id %} highlight-row{% endif %}">
<form method="post">
<input type="hidden" name="form-name" value="update">
<input type="hidden" name="referral-id" value="{{ referral.id }}">
<div class="d-flex mb-3">
<div class="mr-2">
<input name="name"
class="form-control"
required
value="{{ referral.name or '' }}">
</div>
<div>
<button class="btn btn-outline-success">Update</button>
</div>
</div>
</form>
{% set nb_user = referral.nb_user %}
{% set nb_paid_user = referral.nb_paid_user %}
{% if nb_user > 0 %}
<div class="mb-3">
<b class="h1">{{ nb_user }}</b>
{% if nb_user == 1 %}
person
{% else %}
people
{% endif %}
has their online privacy protected thanks to you!
<br />
Among them, <b class="h1">{{ nb_paid_user }}</b>
{% if nb_paid_user == 1 %}
person
{% else %}
people
{% endif %}
has upgraded their accounts.
</div>
{% endif %}
<div class="mt-2">
Please use this referral link to invite your friends trying out SimpleLogin:
<br />
<div class="d-flex mb-5 mt-2" style="max-width: 40em">
<div class="flex-grow-1 mr-2">
<input class="form-control"
id="referral-{{ referral.id }}"
readonly
value="{{ referral.link() }}">
</div>
<div>
<button class="clipboard btn btn-outline-primary"
data-clipboard-action="copy"
data-clipboard-text="{{ referral.link() }}"
data-clipboard-target="#referral-{{ referral.id }}">
Copy <i class="fe fe-clipboard"></i>
</button>
</div>
</div>
</div>
<div class="mb-3">
You can also use the referral code <b>{{ referral.code }}</b> when sharing any link on SimpleLogin.
<br />
Just append
<em data-toggle="tooltip"
title="Click to copy"
class="clipboard"
data-clipboard-text="{{ '?slref=' + referral.code }}"
style="overflow-wrap: break-word">
?slref={{ referral.code }}
</em>
to any link on SimpleLogin website.
</div>
<div>
<form method="post">
<input type="hidden" name="form-name" value="delete">
<input type="hidden" name="referral-id" value="{{ referral.id }}">
<span class="delete-referral float-right btn btn-outline-danger">Delete</span>
</form>
</div>
</div>
{% endfor %}
<form method="post" class="mt-6 card p-4 shadow">
<input type="hidden" name="form-name" value="create">
<div class="form-group">
<input name="code"
class="form-control"
pattern="[0-9a-z-_]{3,}"
placeholder="Referral Code"
title="At least 3 characters. Only lowercase letters, numbers, dashes (-) and underscores (_) are currently supported.">
<div class="small-text">
At least 3 characters. Only lowercase letters, numbers,
dashes (-) and underscores (_) are currently supported.
</div>
</div>
<div class="form-group">
<input name="name"
class="form-control"
required
placeholder="Referral name, something to help you remember why you create it :)">
</div>
<button class="btn btn-success mb-5">Create</button>
</form>
{% if payouts|length > 0 %}
<div class="mt-6 card p-4 shadow">
<h3 class="h4">Payouts</h3>
<table class="table">
<thead>
<tr>
<th>Sent at</th>
<th>Amount</th>
<th>Payment Method</th>
<th>Number of upgraded accounts</th>
</tr>
</thead>
<tbody>
{% for payout in payouts %}
<tr>
<td>{{ payout.created_at | dt }}</td>
<td>${{ payout.amount }}</td>
<td>{{ payout.payment_method }}</td>
<td>{{ payout.number_upgraded_account }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-referral").on("click", function (e) {
let that = $(this);
bootbox.confirm({
message: "This operation is irreversible, please confirm.",
buttons: {
confirm: {
label: 'Yes, delete it',
className: 'btn-danger'
},
cancel: {
label: 'Cancel',
className: 'btn-outline-primary'
}
},
callback: function (result) {
if (result) {
that.closest("form").submit();
}
}
})
});
</script>
{% endblock %}

View File

@ -0,0 +1,89 @@
{% extends "default.html" %}
{% block title %}Quarantine{% endblock %}
{% block head %}
<style>
li {
margin-top: 8px;
}
</style>
{% endblock %}
{% set active_page = "setting" %}
{% block default_content %}
<div class="col">
<h1 class="h3 mb-5">Quarantine & Bounce</h1>
<div class="alert alert-info">
This page shows all emails that are either refused by your mailbox (bounced) or detected as spams/phishing (quarantine) via our
<a href="https://simplelogin.io/docs/getting-started/anti-phishing/"
target="_blank">anti-phishing program ↗</a>
<ul class="p-4 mb-0">
<li>
If the email is indeed spam, this means the alias is now in the hands of a spammer,
you should probably <b>disable</b> the alias.
</li>
<li>
If the email isn't spam and your mailbox refuses the email, we recommend to create a <b>filter</b> to avoid your mailbox provider from blocking legitimate emails. Please refer to
<a href="https://simplelogin.io/docs/getting-started/troubleshooting/#emails-end-up-in-spam"
target="_blank">Setting up filter for SimpleLogin emails ↗</a>
</li>
<li>
If the email is flagged as spams/phishing, this means that the sender explicitly states their emails should respect
<b>DMARC</b> (an email authentication protocol)
and any email that violates this should either be quarantined or rejected. If possible, please contact the sender
so they can update their DMARC setting or fix their SPF/DKIM that cause the DMARC failure.
Their emails are probably being rejected or end up in spam at other email providers as well.
</li>
</ul>
</div>
{% if email_logs|length == 0 %}
<div class="my-4 p-4 card">You don't have any emails in Quarantine/Bounce.</div>
{% endif %}
{% for email_log in email_logs %}
{% set refused_email = email_log.refused_email %}
{% set contact = email_log.contact %}
{% set alias = contact.alias %}
<div class="card p-4 shadow-sm {% if email_log.id == highlight_id %} highlight-row{% endif %}">
<div class="small-text">
Sent {{ refused_email.created_at | dt }}
{% if email_log.bounced %}
<span class="badge badge-info">Bounce</span>
{% else %}
<span class="badge badge-warning">Quarantine</span>
{% endif %}
</div>
{% if email_log.is_reply %}
From: {{ alias.email }}
<br />
To: {{ contact.website_email }}
{% else %}
From: {{ contact.website_email }}
<br />
<span>
To: {{ alias.email }}
<a href='{{ url_for("dashboard.index", highlight_alias_id=alias.id) }}'
class="text-danger small-text"
style="text-decoration: underline">
Disable Alias
</a>
</span>
{% endif %}
{% if refused_email.deleted %}
<div class="mt-6" style="font-weight: 600">Email deleted {{ refused_email.delete_at | dt }}</div>
{% else %}
<a href="{{ refused_email.get_url() }}"
download
class="btn btn-outline-primary mt-4"
style="max-width: 20em">Download →</a>
<div class="small-text">This will download a ".eml" file that you can open in your email client</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,767 @@
{% extends "default.html" %}
{% set active_page = "setting" %}
{% block title %}Settings{% endblock %}
{% block head %}
<style>
.card-title {
font-size: 22px;
font-weight: 600;
margin-bottom: 3px;
}
.highlighted{
border: solid 2px #5675E2;
}
li {
margin-top: 8px;
}
</style>
{% endblock %}
{% block default_content %}
<div class="col pb-3">
<!-- Current plan -->
<div class="card">
<div class="card-body">
<div class="card-title mb-3">Current Plan</div>
{% if current_user.lifetime %}
You have lifetime access to the Premium plan.
{% elif current_user.lifetime_or_active_subscription() %}
{% if paddle_sub %}
<div>
{% if paddle_sub.cancelled %}(Cancelled){% endif %}
{{ paddle_sub.plan_name() }} plan subscribed via Paddle.
<a href="{{ url_for('dashboard.billing') }}">Manage Subscription ➡</a>
</div>
{% endif %}
{% if manual_sub and manual_sub.is_active() %}
<div>
Manual plan which expires {{ manual_sub.end_at | dt }}
({{ manual_sub.end_at.format("YYYY-MM-DD") }}).
{% if manual_sub.is_giveaway %}
<br />
To gain additional features and support SimpleLogin you can upgrade to a Premium plan.
<br />
<a href="{{ url_for('dashboard.pricing') }}"
class="btn btn-sm btn-outline-primary">Upgrade</a>
{% endif %}
</div>
{% endif %}
{% if apple_sub and apple_sub.is_valid() %}
<div>
Premium plan subscribed via Apple which expires {{ apple_sub.expires_date | dt }}
({{ apple_sub.expires_date.format("YYYY-MM-DD") }}).
<div class="alert alert-info">
If you want to subscribe via the Web instead, please make sure to cancel your subscription
on Apple first.
<a href="{{ url_for('dashboard.pricing') }}">
Upgrade <i class="fa fa-arrow-right" aria-hidden="true"></i>
</a>
</div>
</div>
{% endif %}
{% if coinbase_sub and coinbase_sub.is_active() %}
<div>
Yearly plan subscribed with cryptocurrency which expires on
{{ coinbase_sub.end_at.format("YYYY-MM-DD") }}.
<a href="{{ url_for('dashboard.coinbase_checkout_route') }}"
target="_blank">
Extend Subscription <i class="fe fe-external-link"></i>
</a>
</div>
{% endif %}
{% if partner_sub %}<div>Premium subscription managed by {{ partner_name }}.</div>{% endif %}
{% elif current_user.in_trial() %}
Your Premium trial expires {{ current_user.trial_end | dt }}.
{% else %}
You are on the Free plan.
{% endif %}
</div>
</div>
<!-- END Current plan -->
<!-- TOTP -->
<div class="card" id="totp">
<div class="card-body">
<div class="card-title">Two Factor Authentication</div>
<div class="mb-3">
Secure your account with 2FA, you'll be asked for a code generated through an app when you login.
<br />
</div>
{% if not current_user.enable_otp %}
<a href="{{ url_for('dashboard.mfa_setup') }}"
class="btn btn-outline-primary">Setup TOTP</a>
{% else %}
<a href="{{ url_for('dashboard.mfa_cancel') }}"
class="btn btn-outline-danger">Disable TOTP</a>
{% endif %}
</div>
</div>
<!-- END TOTP -->
<!-- WebAuthn -->
<div class="card">
<div class="card-body">
<div class="card-title">Security Key (WebAuthn)</div>
<div class="mb-3">
You can secure your account by linking either your FIDO-supported physical key such as Yubikey, Google
Titan,
or a device with appropriate hardware to your account.
</div>
{% if current_user.fido_uuid is none %}
<a href="{{ url_for('dashboard.fido_setup') }}"
class="btn btn-outline-primary">Setup WebAuthn</a>
{% else %}
<a href="{{ url_for('dashboard.fido_manage') }}"
class="btn btn-outline-info">Manage WebAuthn</a>
{% endif %}
</div>
</div>
<!-- END WebAuthn -->
<!-- Newsletter -->
<div class="card" id="notification">
<div class="card-body">
<div class="card-title">Newsletters</div>
<div class="mb-3">We will occasionally send you emails with new feature announcements.</div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="notification-preference">
<div class="form-check">
<input type="checkbox"
id="notification"
name="notification"
{% if current_user.notification %} checked{% endif %}
class="form-check-input">
<label for="notification">I want to be emailed when new features are released.</label>
</div>
<button type="submit" class="btn btn-outline-primary">Submit</button>
</form>
</div>
</div>
<!-- END Newsletter -->
<!-- Change name & profile picture -->
<div class="card">
<form method="post" enctype="multipart/form-data">
{{ form.csrf_token }}
<input type="hidden" name="form-name" value="update-profile">
<div class="card-body">
<div class="card-title">Profile</div>
<div>
This information will be filled in automatically when you use the
<em>Sign in with SimpleLogin</em> button.
</div>
<div class="form-group mt-3">
<label class="form-label">Name</label>
{{ form.name(class="form-control", value=current_user.name) }}
{{ render_field_errors(form.name) }}
</div>
<div class="form-group">
<div class="form-label">Profile picture</div>
{{ form.profile_picture(class="form-control-file") }}
{{ render_field_errors(form.profile_picture) }}
{% if current_user.profile_picture_id %}
<img src="{{ current_user.profile_picture_url() }}"
class="profile-picture">
{% endif %}
</div>
<button class="btn btn-outline-primary">Update</button>
</div>
</form>
</div>
<!-- END change name & profile picture -->
<!-- Change email -->
<div class="card">
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="form-name" value="update-email">
{{ change_email_form.csrf_token }}
<div class="card-body">
<div class="card-title">Account Email</div>
<div class="mb-3">
This email address is used to log in to SimpleLogin.
<br />
If you want to change the mailbox that emails are forwarded to, use the
<a href="{{ url_for('dashboard.mailbox_route') }}">
<i class="fe fe-inbox"></i> Mailboxes page
</a>
instead.
</div>
<div class="form-group mt-2">
<!-- Not allow user to change email if there's a pending change -->
{{ change_email_form.email(class="form-control", value=current_user.email, readonly=pending_email != None) }}
{{ render_field_errors(change_email_form.email) }}
{% if pending_email %}
<div class="mt-2">
<span class="text-danger">Pending email change: {{ pending_email }}</span>
<a href="{{ url_for('dashboard.resend_email_change') }}"
class="btn btn-secondary btn-sm">
Resend
confirmation email
</a>
<a href="{{ url_for('dashboard.cancel_email_change') }}"
class="btn btn-secondary btn-sm">
Cancel email
change
</a>
</div>
{% endif %}
</div>
<button class="btn btn-outline-primary">Change Email</button>
</div>
</form>
</div>
<!-- END Change email -->
<!-- Connect with Proton -->
{% if connect_with_proton %}
<div class="card" id="connect-with-proton">
<div class="card-body">
<div class="card-title">Connect with Proton</div>
{% if proton_linked_account != None %}
<div class="mb-3">
Your account is currently linked to the Proton account <b>{{ proton_linked_account }}</b>
<br />
</div>
<form method="post"
action="{{ url_for("dashboard.unlink_proton_account") }}">
{{ csrf_form.csrf_token }}
<button class="btn btn-primary mt-2 proton-button">
<img class="mr-2" src="/static/images/proton.svg">
Unlink account
</button>
</form>
{% else %}
<div class="mb-3">
You can connect your Proton and SimpleLogin accounts.
<br>
You can then quickly log in to your SimpleLogin account using the Proton one.
<br>
If you have Proton Unlimited, Business or Visionary, you can have SimpleLogin premium for free.
<br />
</div>
<a class="btn btn-primary mt-2 proton-button"
href="{{ url_for("auth.proton_login", action="link") }}">
<img class="mr-2"
src="{{ url_for('static', filename='images/proton.svg') }}"/>
Connect with Proton
</a>
{% endif %}
</div>
</div>
{% endif %}
<!-- END Connect with Proton -->
<!-- Change password -->
<div class="card" id="change_password">
<div class="card-body">
<div class="card-title">Password</div>
<div class="mb-3">You will receive an email containing instructions on how to change your password.</div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-password">
<button class="btn btn-outline-primary">Change password</button>
</form>
</div>
</div>
<!-- END Change password -->
<!-- Random alias -->
<div id="random-alias" class="card">
<div class="card-body">
<div class="card-title">
Aliases
</div>
<div class="mt-3 mb-1">
Change the way random aliases are generated by default.
</div>
<form method="post" action="#random-alias" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-alias-generator">
<select class="form-control mr-sm-2" name="alias-generator-scheme">
<option value="{{ AliasGeneratorEnum.word.value }}"
{% if current_user.alias_generator == AliasGeneratorEnum.word.value %} selected{% endif %}>
Based on
Random {{ AliasGeneratorEnum.word.name.capitalize() }}
</option>
<option value="{{ AliasGeneratorEnum.uuid.value }}"
{% if current_user.alias_generator == AliasGeneratorEnum.uuid.value %} selected{% endif %}>
Based
on {{ AliasGeneratorEnum.uuid.name.upper() }}
</option>
</select>
<button class="btn btn-outline-primary">
Update
</button>
</form>
<div class="mt-3 mb-1">
Select the default domain for aliases.
</div>
<form method="post" action="#random-alias" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden"
name="form-name"
value="change-random-alias-default-domain">
<select class="form-control mr-sm-2" name="random-alias-default-domain">
<option value="">
Not Selected
</option>
{% for is_public, domain in current_user.available_domains_for_random_alias() %}
<option value="{{ domain }}" {% if current_user.default_alias_custom_domain_id or current_user.default_alias_public_domain_id %}
{% if current_user.default_random_alias_domain() == domain %}
selected {% endif %} {% endif %}>
{{ domain }} (
{% if is_public %}
SimpleLogin domain
{% else %}
your domain
{% endif %}
)
</option>
{% endfor %}
</select>
<button class="btn btn-outline-primary">
Update
</button>
</form>
<div id="random-alias-suffix" class="mt-3 mb-1">
Select the default suffix generator for aliases.
</div>
<form method="post" action="#random-alias-suffix" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="random-alias-suffix">
<select class="form-control mr-sm-2" name="random-alias-suffix-generator">
<option value="0"
{% if current_user.random_alias_suffix==0 %} selected{% endif %}>
Random word from our dictionary
</option>
<option value="1"
{% if current_user.random_alias_suffix==1 %} selected{% endif %}>
Random combination of {{ ALIAS_RAND_SUFFIX_LENGTH }} letter and digits
</option>
</select>
<button class="btn btn-outline-primary">
Update
</button>
</form>
</div>
</div>
<!-- END Random alias -->
<!-- Sender Format -->
<div class="card" id="sender-format">
<div class="card-body">
<div class="card-title">
Sender Address Format
</div>
<div class="mt-1 mb-3">
When your alias receives an email, say from: <b>John Wick &lt;john@wick.com&gt;</b>,
SimpleLogin forwards it to your mailbox.
<br />
Due to some email constraints, SimpleLogin cannot keep the sender email address
in the original form and needs to <b>transform</b> it to one of the formats below.
</div>
<form method="post" action="#sender-format">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-sender-format">
<select class="form-control mr-sm-2" name="sender-format">
<option value="{{ SenderFormatEnum.AT.value }}"
{% if current_user.sender_format == SenderFormatEnum.AT.value %} selected{% endif %}>
John Wick - john at wick.com
</option>
<option value="{{ SenderFormatEnum.A.value }}"
{% if current_user.sender_format == SenderFormatEnum.A.value %} selected{% endif %}>
John Wick - john(a)wick.com
</option>
<option value="{{ SenderFormatEnum.NAME_ONLY.value }}"
{% if current_user.sender_format == SenderFormatEnum.NAME_ONLY.value %} selected{% endif %}>
John Wick
</option>
<option value="{{ SenderFormatEnum.AT_ONLY.value }}"
{% if current_user.sender_format == SenderFormatEnum.AT_ONLY.value %} selected{% endif %}>
john at wick.com
</option>
<option value="{{ SenderFormatEnum.NO_NAME.value }}"
{% if current_user.sender_format == SenderFormatEnum.NO_NAME.value %} selected{% endif %}>
No Name (i.e. only reverse-alias)
</option>
</select>
<button class="btn btn-outline-primary mt-3">
Update
</button>
</form>
</div>
</div>
<!-- END Sender Format -->
<!-- Reverse-alias replacement -->
<div class="card" id="reverse-alias-replacement-section">
<div class="card-body">
<div class="card-title">
Reverse Alias Replacement
<div class="badge badge-warning">
Experimental
</div>
</div>
<div class="mb-3">
When replying to a forwarded email, the <b>reverse-alias</b> can be automatically included
in the attached message by your email client.
If this option is enabled, SimpleLogin will try to <b>replace</b> the reverse-alias by your contact email.
<br />
</div>
<form method="post" action="#reverse-alias-replacement-section">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="replace-ra">
<div class="form-check">
<input type="checkbox"
id="replace-ra"
name="replace-ra"
{% if current_user.replace_reverse_alias %} checked{% endif %}
class="form-check-input">
<label for="replace-ra">
Enable replacing reverse alias
</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
</form>
</div>
</div>
<!-- END Reverse-alias -->
<!-- Sender included in reverse-alias -->
<div class="card" id="sender-in-ra">
<div class="card-body">
<div class="card-title">
Include sender address in reverse-alias
</div>
<div class="mb-3">
If this option is enabled, new reverse-alias will include the sender address (e.g. <span class="italic">sender_domain_com_gibberish@simplelogin.co</span>)
so you can quickly know the sender.
<br />
If disabled, a new reverse-alias will be randomly generated.
Please note that existing reverse-aliases won't change.
</div>
<form method="post" action="#sender-in-ra">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="sender-in-ra">
<div class="form-check">
<input type="checkbox" id="include-sender-ra" name="enable"
{# todo: remove current_user.include_sender_in_reverse_alias is none condition #}
{% if current_user.include_sender_in_reverse_alias is none or current_user.include_sender_in_reverse_alias %}
checked {% endif %} class="form-check-input">
<label for="include-sender-ra">
Include sender address in reverse-alias
</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
</form>
</div>
</div>
<!-- END Reverse-alias -->
<!-- Always expand alias info -->
<div class="card" id="expand-alias-info-section">
<div class="card-body">
<div class="card-title">
Always expand alias info
</div>
<div class="mb-3">
By default, additional alias info is shown after clicking on the "More" button.
<br />
When this option is enabled, alias additional info will always be shown.
<br />
</div>
<form method="post" action="#expand-alias-info-section">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="expand-alias-info">
<div class="form-check">
<input type="checkbox"
id="expand-alias-info"
name="enable"
{% if current_user.expand_alias_info %} checked{% endif %}
class="form-check-input">
<label for="expand-alias-info">
Automatically expand alias info
</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
</form>
</div>
</div>
<!-- END Always expand alias info -->
<!-- Include website address in alias -->
<div class="card" id="include_website_in_one_click_alias">
<div class="card-body">
<div class="card-title">
Include website address in one-click alias creation on browser extension
</div>
<div class="mb-3">
If enabled, the website name will be used as alias prefix
when you create an alias via SimpleLogin browser extension via the email input field
<br />
<img src="https://simplelogin.io/images/one-click-alias.gif"
class="my-2"
style="max-width: 40%">
</div>
<form method="post" action="#include_website_in_one_click_alias">
{{ csrf_form.csrf_token }}
<input type="hidden"
name="form-name"
value="include_website_in_one_click_alias">
<div class="form-check">
<input type="checkbox"
id="include-website-in-alias"
name="enable"
{% if current_user.include_website_in_one_click_alias %} checked{% endif %}
class="form-check-input">
<label for="include-website-in-alias">
Include website address in alias
</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
</form>
</div>
</div>
<!-- END Include website address in alias -->
<!-- Ignore Loop Email -->
{# <div class="card" id="ignore-loop-email-section">#}
{# <div class="card-body">#}
{# <div class="card-title">Ignore Loop Emails</div>#}
{# <div class="mb-3">#}
{# On some email clients, "Reply All" automatically includes your alias that#}
{# would send the same email to your mailbox.#}
{# <br />#}
{# You can disable these "loop" emails by enabling this option.#}
{# </div>#}
{# <form method="post" action="#ignore-loop-email-section">#}
{# {{ csrf_form.csrf_token }} #}
{# <input type="hidden" name="form-name" value="ignore-loop-email">#}
{# <div class="form-check">#}
{# <input type="checkbox" id="ignore-loop-email" name="enable"#}
{# {% if current_user.ignore_loop_email %} checked{% endif %} class="form-check-input">#}
{# <label for="ignore-loop-email">Ignore Loop Emails</label>#}
{# </div>#}
{# <button type="submit" class="btn btn-outline-primary">Update</button>#}
{# </form>#}
{# </div>#}
{# </div>#}
<!-- END Ignore Loop Email -->
<!-- One-click subscribe -->
<div class="card" id="one-click-unsubscribe-section">
<div class="card-body">
<div class="card-title">
One-click unsubscribe
</div>
<div class="mb-3">
On email clients that support the
<a href="https://simplelogin.io/docs/getting-started/one-click-unsubscribe/">
One-click unsubscribe
</a>
button, clicking on it will allow you to do one of these actions:
<ul>
<li>
<b>Original action:</b> Use the same unsubscribe policy set by your sender.
SimpleLogin will make sure to hide your mailbox address.
</li>
<li>
<b>Disable alias:</b> The unsubscribe action will disable the alias that received the email.
</li>
<li>
<b>Block contact:</b> The sender of the email will be blocked: you won't receive emails from this sender to your alias anymore.
</li>
</ul>
</div>
<form method="post"
action="#one-click-unsubscribe-section"
class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="one-click-unsubscribe">
<select class="form-control mr-sm-2" name="unsubscribe-behaviour">
<option value="{{ UnsubscribeBehaviourEnum.PreserveOriginal.name }}" {% if current_user.unsub_behaviour.value == UnsubscribeBehaviourEnum.PreserveOriginal.value %}
selected="selected" {% endif %}>
Preserve the original unsubscribe policy
</option>
<option value="{{ UnsubscribeBehaviourEnum.DisableAlias.name }}" {% if current_user.unsub_behaviour.value == UnsubscribeBehaviourEnum.DisableAlias.value %}
selected="selected" {% endif %}>
Disable the alias that received the email
</option>
<option value="{{ UnsubscribeBehaviourEnum.BlockContact.name }}" {% if current_user.unsub_behaviour.value == UnsubscribeBehaviourEnum.BlockContact.value %}
selected="selected" {% endif %}>
Block the sender that sent the original email
</option>
</select>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
</form>
</div>
</div>
<!-- END One-click subscribe -->
<div class="card">
<div class="card-body">
<div class="card-title">
Quarantine & Bounces
</div>
<div class="mb-3">
When an email is refused (or bounced) by your mailbox provider or flagged by
<a href="https://simplelogin.io/docs/getting-started/anti-phishing/">SimpleLogin anti-phishing program</a>,
SimpleLogin keeps a copy of this email for 7 days so you can take a look at its content and take appropriate actions.
<br />
The emails are deleted in 7 days.
<br />
This is an exceptional case where SimpleLogin temporarily stores the email.
</div>
<a href="{{ url_for('dashboard.refused_email_route') }}"
class="btn btn-outline-primary">
See quarantine & bounce emails
</a>
</div>
</div>
<div class="card" id="blocked-behaviour">
<div class="card-body">
<div class="card-title">
Disabled alias/Blocked contact
</div>
<div class="mb-3">
When an email is sent to a <b>disabled</b> alias or sent from a <b>blocked</b> contact, you can decide what
response the sender should see.
<br />
<b>Ignore</b> means they will see the message as delivered, but SimpleLogin won't actually forward it to you.
This is the default option as you can start receiving the emails again
by re-enabling the alias or unblocking a contact.
<br />
<b>Reject</b> means SimpleLogin will tell them that the alias does not exist.
<br />
</div>
<form method="post" action="#blocked-behaviour" class="form-inline">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="change-blocked-behaviour">
<select class="form-control mr-sm-2" name="blocked-behaviour">
<option value="{{ BlockBehaviourEnum.return_2xx.value }}"
{% if current_user.block_behaviour.value == BlockBehaviourEnum.return_2xx.value %} selected="selected"{% endif %}>
Ignore (the sender will see the email as delivered, but you won't receive anything).
</option>
<option value="{{ BlockBehaviourEnum.return_5xx.value }}"
{% if current_user.block_behaviour.value == BlockBehaviourEnum.return_5xx.value %} selected="selected"{% endif %}>
Reject (the sender will be told that your alias does not exist).
</option>
</select>
<button class="btn btn-outline-primary">
Update
</button>
</form>
</div>
</div>
<div class="card" id="sender-header">
<div class="card-body">
<div class="card-title">
Include original sender in email headers
</div>
<div class="mb-3">
SimpleLogin forwards emails to your mailbox from the <b>reverse-alias</b> and not from the <b>original</b>
sender address.
<br />
If this option is enabled, the original sender addresses is stored in the email header <b>X-SimpleLogin-Envelope-From</b>.
You can choose to display this header in your email client.
<br />
As email headers aren't encrypted, your mailbox service can know the sender address via this header.
</div>
<form method="post" action="#sender-header">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="sender-header">
<div class="form-check">
<input type="checkbox"
id="include-sender-header"
name="enable"
{% if current_user.include_header_email_header %} checked{% endif %}
class="form-check-input">
<label for="include-sender-header">
Include sender address in email headers
</label>
</div>
<button type="submit" class="btn btn-outline-primary">
Update
</button>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title">
Alias import/export
</div>
<div class="mb-3">
You can import your aliases created on other platforms into SimpleLogin.
You can also export your aliases to a readable csv format for a future batch import.
</div>
<a href="{{ url_for('dashboard.batch_import_route') }}"
class="btn btn-outline-primary">
Batch Import
</a>
<a href="{{ url_for('dashboard.alias_export_route') }}"
class="btn btn-outline-secondary">
Export Aliases
</a>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title">
SimpleLogin data export
</div>
<div class="mb-3">
As per GDPR (General Data Protection Regulation) law, you can request a copy of your data which are stored on
SimpleLogin.
A zip file that contains all information will be sent to your SimpleLogin account address.
</div>
<div class="d-flex">
<div>
<form method="post">
{{ csrf_form.csrf_token }}
<input type="hidden" name="form-name" value="send-full-user-report">
<button class="btn btn-outline-info">
Request your data
</button>
</form>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="card-title">
Account Deletion
</div>
<div class="mb-3">
If SimpleLogin isn't the right fit for you, you can simply delete your account.
</div>
<a href="{{ url_for('dashboard.delete_account') }}"
class="btn btn-outline-danger">
Delete account
</a>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
let anchor = window.location.hash;
$(anchor).addClass("highlighted")
</script>
{% endblock %}

View File

@ -0,0 +1,138 @@
{% extends "default.html" %}
{% set active_page = "subdomain" %}
{% block title %}Subdomains{% endblock %}
{% block head %}{% endblock %}
{% block default_content %}
<div class="row">
<div class="col">
<h1 class="h3">
Subdomains
<a class="ml-3 text-info"
style="font-size: 12px"
data-toggle="collapse"
href="#howtouse"
role="button"
aria-expanded="false"
aria-controls="collapseExample">
How to use <i class="fe fe-chevrons-down"></i>
</a>
</h1>
{% if not current_user.is_premium() %}
<div class="alert alert-danger" role="alert">
This feature is only available on Premium plan.
<a href="{{ url_for('dashboard.pricing') }}"
target="_blank"
rel="noopener">
Upgrade<i class="fe fe-external-link"></i>
</a>
</div>
{% endif %}
<div class="alert alert-primary collapse {% if not subdomains %} show{% endif %}"
id="howtouse"
role="alert">
You can use subdomain to quickly create email aliases without opening SimpleLogin app.
<br />
Handy when you need to quickly give out an email address, for example on a phone call, in a meeting or just
anywhere you want.
<br />
After choosing a subdomain, simply use <b>anything@my-subdomain.simplelogin.co</b>
next time you need an alias:
it'll be <b>automatically created</b> the first time it receives an email.
<br />
</div>
<div class="row">
{% for subdomain in subdomains %}
<div class="col-12 col-lg-6">
<div class="card" style="">
<div class="card-body">
<h5 class="card-title">
<a href="{{ url_for('dashboard.domain_detail', custom_domain_id=subdomain.id) }}">{{ subdomain.domain }}</a>
</h5>
<h6 class="card-subtitle mb-4 text-muted">
Created {{ subdomain.created_at | dt }}
<br />
<span class="font-weight-bold">{{ subdomain.nb_alias() }}</span> aliases.
<br />
</h6>
<a href="{{ url_for('dashboard.domain_detail', custom_domain_id=subdomain.id) }}"
class="mt-3">Details ➡</a>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row {% if current_user.subdomain_quota <= 0 %} disabled-content{% endif %}"
id="new-subdomain">
<div class="col">
<div class="card">
<div class="card-body">
<h2 class="h4 mb-1">New Subdomain</h2>
<form method="post" class="mt-2" data-parsley-validate>
<input type="hidden" name="form-name" value="create">
<div class="form-group">
<label>Subdomain</label>
<input name="subdomain"
v-model="subdomain"
data-parsley-pattern="[0-9a-z-]{3,}"
data-parsley-trigger="change"
data-parsley-error-message="At least 3 characters. Only lowercase letters, numbers and dashes (-) are supported."
class="form-control"
minlength="3"
maxlength="32">
</div>
<div class="form-group">
<label>Root domain</label>
<select name="domain" v-model="domain" class="form-control">
{% for sl_domain in sl_domains %}
<option value="{{ sl_domain.domain }}">
{{ sl_domain.domain }}
</option>
{% endfor %}
</select>
</div>
<button class="btn btn-primary">Create</button>
</form>
<div v-if="toShow" class="text-info mt-2">
You are about to create <b>[[ subdomain]].[[domain]]</b> subdomain.
</div>
<div class="alert alert-info mt-3" style="font-size: 12px">
Deleting a subdomain will <b>not</b> restore the subdomain quota
so please make sure
to choose the subdomain you want to keep.
<br />
Currently you can create up to <b style="font-size: 1.5em">{{ current_user.subdomain_quota }}</b>
subdomains.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
new Vue({
el: '#new-subdomain',
delimiters: ["[[", "]]"], // necessary to avoid conflict with jinja
data: {
subdomain: "",
domain: "{{ sl_domains[0].domain }}"
},
computed: {
toShow: function () {
if (this.subdomain && this.domain) {
return true;
}
return false;
}
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,104 @@
{% extends "default.html" %}
{% set active_page = 'dashboard' %}
{% block title %}Support{% endblock %}
{% block default_content %}
<div class="col pb-3">
<div class="card">
<div class="card-body">
<div class="card-title mb-3">Report a problem</div>
<div class="alert alert-info">
If an email cannot be delivered to your mailbox, please check
<a href="{{ url_for('dashboard.notifications_route') }}">
your
notifications
</a>
for error messages.
<br />
For generic questions, i.e. not related to your account, we recommend to post the question on
our
<a href="https://www.reddit.com/r/Simplelogin/">Reddit</a>
where our community can help answer the question
and other people with the same question can find the answer there.
</div>
<div class="alert alert-warning">
A support ticket will be created in Zendesk. Please do not include any sensitive information in the ticket.
</div>
<form id="supportZendeskForm" method="post" enctype="multipart/form-data">
<div class="mt-4 mb-5">
<label for="issueDescription" class="form-label font-weight-bold">What happened?</label>
<textarea class="form-control" required name="ticket_content" id="issueDescription" rows="3" placeholder="Please provide as much information as possible. For example which alias(es), mailbox(es) ar affected, if this is a persistent issue...">{{- ticket_content or '' -}}</textarea>
</div>
<div class="mt-5 font-weight-bold">Attach files to support request</div>
<div class="text-muted">Only images, text and emails are accepted</div>
<div class="custom-file">
<input type="file"
class="custom-file-input"
name="ticket_files"
id="ticketFileGroup"
multiple="multiple">
<label class="custom-file-label" for="ticketFileGroup">Choose file</label>
</div>
<div class="mt-5 font-weight-bold">Where can we reach you?</div>
<div class="text-muted">
Conversations related to this ticket will be sent to this address. Feel free to use an alias here.
</div>
<div class="input-group mb-3" id="alias-group">
<input type="text"
required
class="form-control"
placeholder="Email"
name="ticket_email"
v-model='ticket_email'
aria-label="Email to send responses to"
aria-describedby="button-addon2">
<div class="input-group-append">
<button class="btn btn-outline-primary"
type="button"
@click="generateRandomAlias"
id="button-addon2">
Generate a random alias
</button>
</div>
</div>
<div class="mt-5">
<button class="btn btn-primary">Create support ticket</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
new Vue({
el: '#alias-group',
data: {
ticket_email: '{{ ticket_email }}'
},
methods: {
generateRandomAlias: async function (event) {
let result = await fetch('/api/alias/random/new', {method: 'POST'});
if (result.ok) {
let data = await result.json();
this.ticket_email = data.alias;
} else if (result.status === 429 ){
toastr.warning("You've created too many aliases recently. Wait a bit before creating more");
} else {
toastr.warning("You can't create more aliases");
}
}
}
});
$('.custom-file input').change(function (e) {
let files = [];
for (let i = 0; i < $(this)[0].files.length; i++) {
files.push($(this)[0].files[i].name);
}
$(this).next('.custom-file-label').html(files.join(', '));
});
</script>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "default.html" %}
{% set active_page = "dashboard" %}
{% block title %}Block an alias{% endblock %}
{% block default_content %}
<div class="card">
<div class="card-body">
<h1 class="h3">Block alias</h1>
<p>
You are about to block the alias
<a href="mailto:{{ alias }}">{{ alias }}</a>
</p>
<p>After this, you will stop receiving all emails sent to this alias, please confirm.</p>
<form method="post">
<button class="btn btn-warning">Confirm</button>
</form>
</div>
</div>
{% endblock %}