Skip to content

Commit

Permalink
feat: added ability to enable IMAP for an alias, updated docs, fixed …
Browse files Browse the repository at this point in the history
…tests, synced locales
  • Loading branch information
titanism committed Nov 7, 2023
1 parent c66e545 commit 1fe042e
Show file tree
Hide file tree
Showing 34 changed files with 2,352 additions and 1,011 deletions.
6 changes: 5 additions & 1 deletion app/controllers/web/my-account/validate-alias.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ function validateAlias(ctx, next) {
);
else body.recipients = [];

if (_.isEmpty(body.recipients)) body.recipients = [ctx.state.user.email];
if (typeof ctx.request.body.has_imap !== 'undefined' || !ctx.api)
body.has_imap = boolean(ctx.request.body.has_imap);

if (ctx.api && _.isEmpty(body.recipients))
body.recipients = [ctx.state.user.email];

ctx.state.body = body;

Expand Down
25 changes: 23 additions & 2 deletions app/models/aliases.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,13 @@ Aliases.pre('validate', function (next) {
if (!isSANB(this.description)) this.description = undefined;

// alias must have at least one recipient
if (!_.isArray(this.recipients) || _.isEmpty(this.recipients))
return next(new Error('Alias must have at least one recipient.'));
if (
!this.has_imap &&
(!_.isArray(this.recipients) || _.isEmpty(this.recipients))
)
return next(
new Error('Alias must have at least one recipient or IMAP enabled.')
);

next();
});
Expand All @@ -299,6 +304,17 @@ Aliases.pre('validate', function (next) {
next();
});

// user cannot have imap enabled on a catchall nor regex
Aliases.pre('validate', function (next) {
if (this.has_imap && (this.name === '*' || this.name.startsWith('/')))
return next(
new Error(
'Alias cannot have IMAP enabled with a catch-all nor regex name.'
)
);
next();
});

// this must be kept before other `pre('save')` hooks as
// it populates "id" String automatically for comparisons
Aliases.plugin(mongooseCommonPlugin, {
Expand Down Expand Up @@ -391,6 +407,11 @@ Aliases.pre('save', async function (next) {
i18n.translateError('CANNOT_CREATE_REGEX_ON_GLOBAL', alias.locale)
);

if (domain.is_global && alias.has_imap)
throw Boom.badRequest(
i18n.translateError('CANNOT_USE_IMAP_ON_GLOBAL', alias.locale)
);

if (domain.is_catchall_regex_disabled && alias.name === '*')
throw Boom.badRequest(
i18n.translateError('CANNOT_CREATE_CATCHALL_ON_DOMAIN', alias.locale)
Expand Down
20 changes: 11 additions & 9 deletions app/views/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,8 @@ curl BASE_URI/v1/domains/DOMAIN_NAME/aliases \
| `description` | No | String | Alias description |
| `labels` | No | String or Array | List of labels (must be line-break/space/comma separated String or Array) |
| `has_recipient_verification` | No | Boolean | Whether to enable to require recipients to click an email verification link for emails to flow through (defaults to the domain's setting if not explicitly set in the request body) |
| `is_enabled` | No | Boolean | Whether to enable to disable this alias (if disabled, emails will be routed nowhere but return successful status codes). Defaults to `true`, but if a value is passed, it is converted to a boolean using [boolean](https://github.com/thenativeweb/boolean#quick-start)) |
| `is_enabled` | No | Boolean | Whether to enable to disable this alias (if disabled, emails will be routed nowhere but return successful status codes). If a value is passed, it is converted to a boolean using [boolean](https://github.com/thenativeweb/boolean#quick-start)) |
| `has_imap` | No | Boolean | Whether to enable to disable IMAP storage for this alias (if disabled, then inbound emails received will not get stored to [IMAP storage](/encrypted-email). If a value is passed, it is converted to a boolean using [boolean](https://github.com/thenativeweb/boolean#quick-start)) |

> Example Request:
Expand Down Expand Up @@ -561,14 +562,15 @@ curl BASE_URI/v1/domains/:domain_name/aliases/:alias_name \

> `PUT /v1/domains/DOMAIN_NAME/aliases/ALIAS_ID`
| Body Parameter | Required | Type | Description |
| ---------------------------- | -------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | No | String | Alias name |
| `recipients` | Yes | String or Array | List of recipients (must be line-break/space/comma separated String or Array of valid email addresses, fully-qualified domain names ("FQDN"), IP addresses, and/or webhook URL's) |
| `description` | No | String | Alias description |
| `labels` | No | String or Array | List of labels (must be line-break/space/comma separated String or Array) |
| `has_recipient_verification` | No | Boolean | Whether to enable to require recipients to click an email verification link for emails to flow through (defaults to the domain's setting if not explicitly set in the request body) |
| `is_enabled` | No | Boolean | Whether to enable to disable this alias (if disabled, emails will be routed nowhere but return successful status codes) |
| Body Parameter | Required | Type | Description |
| ---------------------------- | -------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | No | String | Alias name |
| `recipients` | No | String or Array | List of recipients (must be line-break/space/comma separated String or Array of valid email addresses, fully-qualified domain names ("FQDN"), IP addresses, and/or webhook URL's) |
| `description` | No | String | Alias description |
| `labels` | No | String or Array | List of labels (must be line-break/space/comma separated String or Array) |
| `has_recipient_verification` | No | Boolean | Whether to enable to require recipients to click an email verification link for emails to flow through (defaults to the domain's setting if not explicitly set in the request body) |
| `is_enabled` | No | Boolean | Whether to enable to disable this alias (if disabled, emails will be routed nowhere but return successful status codes) |
| `has_imap` | No | Boolean | Whether to enable to disable IMAP storage for this alias (if disabled, then inbound emails received will not get stored to [IMAP storage](/encrypted-email). If a value is passed, it is converted to a boolean using [boolean](https://github.com/thenativeweb/boolean#quick-start)) |

> Example Request:
Expand Down
25 changes: 23 additions & 2 deletions app/views/my-account/domains/aliases/_form.pug
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,32 @@ if alias
i.fa.fa-plus
= " "
= t("Add new domain")
if !domain || !domain.is_global
.form-group
= t("IMAP Storage")
= " "
span.text-muted= t("(optional)")
.alert.alert-success
.form-check.text-left.small
input#input-has-imap.form-check-input(
type="checkbox",
name="has_imap",
checked=alias ? alias.has_imap : true,
value="true"
)
label.form-check-label(
for='input-has-imap'
)
= t("This will enable our new IMAP storage feature for this alias.")
= " "
= t("You can have both IMAP storage and forwarding recipients enabled at the same time.")
= " "
!= t('If you would like to learn more about our new IMAP storage feature, please <a class="alert-link" href="%s" target="_blank">click here to read our deep dive on Encrypted Email.</a>', l('/encrypted-email'))
.form-group
label(for="textarea-alias-recipients")
= t("Forwarding Recipients")
= " "
span.text-danger= t("(required)")
span.text-muted= t("(optional)")
textarea#textarea-alias-recipients.form-control(name="recipients", rows=1)= alias && Array.isArray(alias.recipients) ? alias.recipients.join(", ") : user.email
p.form-text.small.text-black.text-themed-50= t('Recipients must be a line-break/space/comma separated list of valid email addresses, fully-qualified domain names ("FQDN"), IP addresses, and/or webhook URL\'s. We will automatically remove duplicate entries for you and perform validation when you click "Continue" below.')
.form-group
Expand Down Expand Up @@ -114,7 +135,7 @@ if !domain || (domain && !domain.is_global)
checked=alias ? alias.has_recipient_verification : domain && domain.has_recipient_verification
)
label.form-check-label(for="input-has-recipient-verification")= t("Requires recipients to click email verification link")
p.form-text.small.text-black.text-themed-50= t("If you check this, then each email recipient will be required to click an email verification link in order for emails to flow through.")
p.form-text.small.text-black.text-themed-50= t("If you check this, then each forwarding recipient will be required to click an email verification link in order for emails to flow through.")
.form-group.form-check
input#input-is-enabled.form-check-input(
type="checkbox",
Expand Down
3 changes: 2 additions & 1 deletion config/phrases.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ module.exports = {
'The target <span class="notranslate">%s</span> has recently blocked this IP address, please try again in 1 hour.',
CANNOT_CREATE_CATCHALL_ON_GLOBAL: 'Cannot create catch-all on global domain.',
CANNOT_CREATE_REGEX_ON_GLOBAL: 'Cannot create regex on global domain.',
CANNOT_USE_IMAP_ON_GLOBAL: 'Cannot use IMAP on global domain.',
CANNOT_CREATE_CATCHALL_ON_DOMAIN:
'Cannot create catch-all on domain due to large alias volume size.',
CANNOT_CREATE_REGEX_ON_DOMAIN:
Expand All @@ -69,7 +70,7 @@ module.exports = {
CANNOT_CREATE_TOKEN_FOR_CATCHALL: 'Cannot create token for catch-all alias.',
CANNOT_CREATE_TOKEN_FOR_REGEX: 'Cannot create token for regex alias.',
ALIAS_PASSWORD_EMAIL:
'<p><span class="notranslate text-monospace font-weight-bold">%s</span> has sent you a password to use for <span class="notranslate text-monospace font-weight-bold">%s</span>.</p><p><a href="%s" rel="noopener noreferrer" class="font-weight-bold text-decoration-underline" target="_blank">Click this link</a> within the next 3 days and follow the instructions.</p>',
'<p><span class="notranslate text-monospace font-weight-bold">%s</span> has sent you a password to use for <span class="notranslate text-monospace font-weight-bold">%s</span>.</p><p><a href="%s" rel="noopener noreferrer" class="font-weight-bold text-decoration-underline" target="_blank">Click this link</a> and immediately follow the instructions.</p>',
ALIAS_GENERATED_PASSWORD:
'You have successfully generated the alias password below &ndash; you must copy and securely store it before closing this window. <strong class="text-decoration-underline"><br /><br />The password below will not be shown again once you click OK.</strong><br /><br /><strong>Username:</strong> <code class="notranslate">%s</code><br /><br /><strong>Password:</strong> <code class="notranslate">%s</code><br /><br /><br /><br />This window will automatically close in 30 seconds.<br />',
PAST_DUE_OR_INVALID_ADMIN:
Expand Down
4 changes: 1 addition & 3 deletions imap-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ const refreshSession = require('#helpers/refresh-session');

// TODO: send welcome email to user in their sqlite dbs
// TODO: restore locales, then run through pages, then mandarin
// TODO: alias storage, pooled, etc
// TODO: alias has to either have at least one recipient or has_imap everywhere (including via API)
// TODO: alias bootstrap progress bars for storage, pooled, etc
// TODO: alias.has_imap validation on IMAP connection
// TODO: when user deletes account then also purge sqlite databases and backups
// TODO: automated job to detect files on block storage and R2 that don't correspond to actual aliases
Expand All @@ -53,7 +52,6 @@ const refreshSession = require('#helpers/refresh-session');
// TODO: auto-reply/vacation responder
// TODO: enforce maxDownload and maxUpload
// TODO: enforce 10-15 max connections per alias
// TODO: when user generates new password, if any existing were found, then prompt them to enter current password, complete captcha, and type out "I understand this message" and check a checkbox
// TODO: each R2 bucket seems like it's 18 TB max?

// TODO: future items
Expand Down
Loading

0 comments on commit 1fe042e

Please sign in to comment.