Browse Source

initial commit

master
ale 4 years ago
commit
8bcc2dbbcd
161 changed files with 7253 additions and 0 deletions
  1. +2
    -0
      .env
  2. +1
    -0
      .gitignore
  3. +20
    -0
      README.md
  4. +78
    -0
      docker-compose.yml
  5. +7
    -0
      entrypoint.sh
  6. +0
    -0
      secure/.gitignore
  7. +9
    -0
      start.sh
  8. +13
    -0
      webmail/Dockerfile
  9. +78
    -0
      webmail/config/default.toml
  10. +28
    -0
      webmail/config/development.toml
  11. +85
    -0
      webmail/views/account/2fa.hbs
  12. +142
    -0
      webmail/views/account/autoreply.hbs
  13. +140
    -0
      webmail/views/account/create.hbs
  14. +88
    -0
      webmail/views/account/filters.hbs
  15. +18
    -0
      webmail/views/account/filters/create.hbs
  16. +18
    -0
      webmail/views/account/filters/edit.hbs
  17. +131
    -0
      webmail/views/account/identities.hbs
  18. +46
    -0
      webmail/views/account/identities/create.hbs
  19. +46
    -0
      webmail/views/account/identities/edit.hbs
  20. +103
    -0
      webmail/views/account/index.hbs
  21. +68
    -0
      webmail/views/account/login.hbs
  22. +52
    -0
      webmail/views/account/login.hbs.new
  23. +68
    -0
      webmail/views/account/login.hbs.orig
  24. +98
    -0
      webmail/views/account/profile.hbs
  25. +26
    -0
      webmail/views/account/security.hbs
  26. +131
    -0
      webmail/views/account/security/2fa.hbs
  27. +35
    -0
      webmail/views/account/security/asp.hbs
  28. +151
    -0
      webmail/views/account/security/asps.hbs
  29. +34
    -0
      webmail/views/account/security/enable-totp.hbs
  30. +45
    -0
      webmail/views/account/security/enable-u2f.hbs
  31. +115
    -0
      webmail/views/account/security/events.hbs
  32. +98
    -0
      webmail/views/account/security/gpg.hbs
  33. +67
    -0
      webmail/views/account/security/password.hbs
  34. +43
    -0
      webmail/views/account/update-password.hbs
  35. +7
    -0
      webmail/views/error.hbs
  36. +160
    -0
      webmail/views/help.hbs
  37. +3
    -0
      webmail/views/index.hbs
  38. +50
    -0
      webmail/views/layout-popup.hbs
  39. +67
    -0
      webmail/views/layout-webmail.hbs
  40. +30
    -0
      webmail/views/layout.hbs
  41. +3
    -0
      webmail/views/partials/accountmenu.hbs
  42. +150
    -0
      webmail/views/partials/filter.hbs
  43. +18
    -0
      webmail/views/partials/header.hbs
  44. +68
    -0
      webmail/views/partials/identity.hbs
  45. +44
    -0
      webmail/views/partials/mailbox.hbs
  46. +51
    -0
      webmail/views/partials/messagerow.hbs
  47. +85
    -0
      webmail/views/partials/navbar.hbs
  48. +71
    -0
      webmail/views/partials/scripts.hbs
  49. +8
    -0
      webmail/views/partials/searchfield.hbs
  50. +5
    -0
      webmail/views/partials/securitymenu.hbs
  51. +57
    -0
      webmail/views/partials/tos.hbs
  52. +13
    -0
      webmail/views/tos.hbs
  53. +134
    -0
      webmail/views/webmail/audit.hbs
  54. +46
    -0
      webmail/views/webmail/create.hbs
  55. +754
    -0
      webmail/views/webmail/index.hbs
  56. +75
    -0
      webmail/views/webmail/mailbox.hbs
  57. +630
    -0
      webmail/views/webmail/message.hbs
  58. +382
    -0
      webmail/views/webmail/send.hbs
  59. +24
    -0
      wildduck/Dockerfile
  60. +0
    -0
      wildduck/haraka/attachments/.gitignore
  61. +13
    -0
      wildduck/haraka/config/access.domains
  62. +6
    -0
      wildduck/haraka/config/access.ini
  63. +14
    -0
      wildduck/haraka/config/aliases
  64. +2
    -0
      wildduck/haraka/config/attachment.ctype.regex
  65. +1
    -0
      wildduck/haraka/config/attachment.filename.regex
  66. +5
    -0
      wildduck/haraka/config/auth_flat_file.ini
  67. +7
    -0
      wildduck/haraka/config/auth_vpopmaild.ini
  68. +5
    -0
      wildduck/haraka/config/avg.ini
  69. +18
    -0
      wildduck/haraka/config/bounce.ini
  70. +5
    -0
      wildduck/haraka/config/clamd.ini
  71. +62
    -0
      wildduck/haraka/config/data.headers.ini
  72. +202
    -0
      wildduck/haraka/config/data.uribl.excludes
  73. +37
    -0
      wildduck/haraka/config/data.uribl.ini
  74. +1
    -0
      wildduck/haraka/config/databytes
  75. +7
    -0
      wildduck/haraka/config/delay_deny.ini
  76. +8
    -0
      wildduck/haraka/config/dhparams.pem
  77. +5
    -0
      wildduck/haraka/config/dkim_sign.ini
  78. +23
    -0
      wildduck/haraka/config/dnsbl.ini
  79. +11
    -0
      wildduck/haraka/config/early_talker.ini
  80. +14
    -0
      wildduck/haraka/config/fcrdns.ini
  81. +43
    -0
      wildduck/haraka/config/greylist.ini
  82. +57
    -0
      wildduck/haraka/config/helo.checks.ini
  83. +2
    -0
      wildduck/haraka/config/host_list
  84. +6
    -0
      wildduck/haraka/config/host_list_regex
  85. +7
    -0
      wildduck/haraka/config/http.ini
  86. +1
    -0
      wildduck/haraka/config/internalcmd_key
  87. +7
    -0
      wildduck/haraka/config/lmtp.ini
  88. +11
    -0
      wildduck/haraka/config/log.ini
  89. +12
    -0
      wildduck/haraka/config/lookup_rdns.strict.ini
  90. +1
    -0
      wildduck/haraka/config/lookup_rdns.strict.timeout
  91. +1
    -0
      wildduck/haraka/config/lookup_rdns.strict.whitelist
  92. +5
    -0
      wildduck/haraka/config/lookup_rdns.strict.whitelist_regex
  93. +4
    -0
      wildduck/haraka/config/mail_from.is_resolvable.ini
  94. +1
    -0
      wildduck/haraka/config/max_unrecognized_commands
  95. +1
    -0
      wildduck/haraka/config/me
  96. +18
    -0
      wildduck/haraka/config/messagesniffer.ini
  97. +30
    -0
      wildduck/haraka/config/mongodb.ini
  98. +15
    -0
      wildduck/haraka/config/outbound.bounce_message
  99. +36
    -0
      wildduck/haraka/config/outbound.bounce_message_html
  100. +106
    -0
      wildduck/haraka/config/outbound.bounce_message_image

+ 2
- 0
.env View File

@ -0,0 +1,2 @@
DOMAIN=domain.com
REVERSE_DNS=com.domain

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
secure

+ 20
- 0
README.md View File

@ -0,0 +1,20 @@
# Haraka-Wildduck Docker Mail Server
## Instalar
- Ejecutar: `./start.sh` <dominio> - Configura los certificados en la carpeta ./secure
- Editar `.env` con el valor del dominio
## Arrancar
- Instalar `docker` y `docker-compose`
- Ejecutar: docker-compose up -d
- Abrir el navegador http://webmail:3000
## Persistencia
- Ejecutar: docker cp mongo:/data/db ./mongodb && chown -R 999.999 ./mongodb
- Ejecutar: docker cp redis:/data ./redis && chown -R 999.999 ./redis
- Descomentar las lineas del archivo `docker-compose.yml`
- Ejecutar: docker-compose down && docker-compose up -d
### Licencia
- MIT

+ 78
- 0
docker-compose.yml View File

@ -0,0 +1,78 @@
version: '3'
services:
wildduck:
build:
context: ./wildduck
args:
DOMAIN: $DOMAIN
REVERSE_DNS: $REVERSE_DNS
hostname: wildduck
container_name: wildduck
restart: always
entrypoint:
- /bin/bash
- /entrypoint.sh
ports:
- "25:25/tcp"
- "465:465/tcp"
- "993:993/tcp"
expose:
- 80
- 12080
volumes:
- ./entrypoint.sh:/entrypoint.sh:ro
- ./secure:/secure:ro
- ./wildduck/haraka/attachments:/home/node/Haraka/attachments
depends_on:
- redis
- mongo
networks:
mailnet:
redis:
image: redis
hostname: redis
container_name: redis
restart: always
# volumes:
# - ./redis:/data
expose:
- 6379
networks:
mailnet:
mongo:
image: mongo
hostname: mongo
container_name: mongo
restart: always
# volumes:
# - ./mongodb:/data/db
expose:
- 27017
networks:
mailnet:
webmail:
build:
context: ./webmail
args:
DOMAIN: $DOMAIN
hostname: webmail
container_name: webmail
restart: always
entrypoint:
- node
- server.js
- --config=/webmail/config/default.toml
ports:
- "3000:3000/tcp"
depends_on:
- redis
- mongo
- wildduck
networks:
mailnet:
networks:
mailnet:

+ 7
- 0
entrypoint.sh View File

@ -0,0 +1,7 @@
#!/bin/bash
cd /haraka
node haraka.js &
cd /wildduck
node server.js &
cd /wildduck-mta
npm start --production

+ 0
- 0
secure/.gitignore View File


+ 9
- 0
start.sh View File

@ -0,0 +1,9 @@
#!/bin/bash
if [[ ! -z $1 ]]; then
sudo apt install -y opendkim-tools openssl
rm -f ./secure/*
openssl req -newkey rsa:2048 -nodes -keyout ./secure/privkey.pem -x509 -days 365 -subj "/CN=$1" -out ./secure/fullchain.pem
opendkim-genkey -b 2048 -h rsa-sha256 -r -s dkim -d "$1" --directory ./secure
else
echo -e "- Necesita indicar un dominio\nEjemplo: ./start.sh domain.com"
fi

+ 13
- 0
webmail/Dockerfile View File

@ -0,0 +1,13 @@
FROM node:8-slim
ARG DOMAIN
RUN apt update && apt -y install git python make
RUN git clone https://github.com/nodemailer/wildduck-webmail /webmail
WORKDIR /webmail
RUN git checkout 5c54625a8b192823184ba7f5da41f3414e76db94
COPY ./config /webmail/config
COPY ./views /webmail/views
RUN chown node.node -R /webmail
USER node
RUN npm install
RUN npm run bowerdeps
RUN find ./config ./views -type f -exec sed -i "s/{{DOMAIN}}/$DOMAIN/g" {} +

+ 78
- 0
webmail/config/default.toml View File

@ -0,0 +1,78 @@
name="webmail.{{DOMAIN}}"
title="Wild Duck Mail"
[service]
# email domain for new users
domain="{{DOMAIN}}"
# default quotas for new users
quota=1024
recipients=2000
forwards=2000
identities=10
allowIdentityEdit=true
allowJoin=true
enableSpecial=true # if true the allow creating addresses with special usernames
# allowed domains for new addresses
domains=["{{DOMAIN}}"]
[api]
# url="http://127.0.0.1:8080"
# accessToken=""
url="http://wildduck"
accessToken="notoken"
[dbs]
# mongodb connection string for the main database
mongo="mongodb://mongo:27017/wildduck"
# redis connection string for Express sessions
redis="redis://redis:6379/3"
[www]
host="webmail"
port=3000
proxy=true
postsize="5MB"
log="dev"
secret="secret times"
secure=false
# baseurl="https://webmail.{{DOMAIN}}"
listSize=20
[recaptcha]
enabled=false
siteKey=""
secretKey=""
[totp]
# Issuer name for TOTP, defaults to config.name
issuer=false
# once setup do not change as it would invalidate all existing 2fa sessions
secret="a secret cat"
[u2f]
# set to false if not using HTTPS
enabled=false
# must be https url or use default
#appId="https://127.0.0.1:8080"
appId="https://webmail.{{DOMAIN}}"
[log]
level="silly"
mail=true
[setup]
# these values are shown in the configuration help page
[setup.imap]
hostname="imap.{{DOMAIN}}"
secure=true
port=993
[setup.pop3]
hostname="imap.{{DOMAIN}}"
secure=true
port=993
[setup.smtp]
hostname="smtp.{{DOMAIN}}"
secure=true
port=465

+ 28
- 0
webmail/config/development.toml View File

@ -0,0 +1,28 @@
name="Wild Duck Mail Temporary"
[service]
# email domain for new users
domain="local.tahvel.info"
# default quotas for new users
quota=102400
# allowed domains for new addresses
domains=["local.tahvel.info", "example.com"]
[www]
proxy=true
baseurl="https://local.tahvel.info"
[setup]
# these values are shown in the configuration help page
[setup.imap]
hostname="local.tahvel.info"
secure=true
port=993
[setup.pop3]
hostname="local.tahvel.info"
secure=true
port=995
[setup.smtp]
hostname="local.tahvel.info"
secure=false
port=587

+ 85
- 0
webmail/views/account/2fa.hbs View File

@ -0,0 +1,85 @@
<input type="hidden" id="_csrf" value="{{csrfToken}}" />
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Two factor authentication</h3>
</div>
<div class="panel-body">
<div id="show-u2f" style="display:{{#if enabledU2f}}block{{else}}none{{/if}}">
<div style="margin:10px 0;">
<div id="u2f-wait">
<img src="/images/u2f-wait.png" />
</div>
<div id="u2f-fail" style="display: none">
<img src="/images/u2f-fail.png" />
</div>
<div id="u2f-success" style="display: none">
<img src="/images/u2f-success.png" />
</div>
</div>
<p id="message">
Initializing...
</p>
<div>
<div class="pull-right">
<a href="#" id="enable-totp">or use security code</a>
</div>
<a href="/account/logout"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> Cancel</a>
</div>
</div>
<div id="show-totp" style="display:{{#if enabledU2f}}none{{else}}block{{/if}}">
<form id="totp-form">
<p>
Open your authentication app and enter the code to log in
</p>
<div class="form-group" id="totp-token-field">
<label for="token">Security code</label>
<input type="number" class="form-control" id="token" placeholder="6 digit code" required autofocus>
<span class="help-block" id="totp-token-error" style="display: none"></span>
</div>
<div>
<div class="pull-right">
<button type="submit" id="totp-btn" class="btn btn-success" data-loading-text="Checking..."><span class="glyphicon glyphicon-ok" aria-hidden="true"></span> Verify</button>
</div>
<a href="/account/logout"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> Cancel</a>
</div>
<div class="clearfix"></div>
</form>
</div>
<div class="checkbox form-footer">
<label>
<input type="checkbox" id="remember2fa"> Trust this device for 30 days
</label>
</div>
</div>
</div>
<script>
// U2F support must be checked *before* loading /u2f-api.js
{{! only check if user has enabled u2f, otherwise no reason to use it }}
var U2FSUPPORT = {{#if enabledU2f}}typeof u2f === 'object' || typeof chrome === 'object'{{else}}false{{/if}};
</script>
<script src="/login-key-handler.js"></script>
<script src="/u2f-api.js"></script>
<script src="/2fa.js"></script>

+ 142
- 0
webmail/views/account/autoreply.hbs View File

@ -0,0 +1,142 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-calendar" aria-hidden="true"></span> Autoreply</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form method="post" action="/account/autoreply">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" id="start" name="start" value="{{#if values.start}}{{values.start}}{{/if}}" />
<input type="hidden" id="end" name="end" value="{{#if values.end}}{{values.end}}{{/if}}" />
<fieldset>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Autoreply settings</h3>
</div>
<div class="panel-body">
<p>
If enabled then an autoreply message is sent to all incoming messages. If a contact sends multiple messages then the autoreply is sent at most once in every four hours.
</p>
<div class="radio">
<label>
<input type="radio" name="status" value="false" {{#unless values.status}}checked{{/unless}}>
Autoreply is {{#unless values.status}}<span class="label label-default">disabled</span>{{else}}disabled{{/unless}}
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="status" value="true" {{#if values.status}}checked{{/if}}>
Autoreply is {{#if values.status}}<span class="label label-info">enabled</span>{{else}}enabled{{/if}}
</label>
</div>
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" name="name" value="{{values.name}}" placeholder="Sender name in the autoreply From: header">
</div>
<div class="form-group">
<label for="subject">Subject</label>
<input type="text" class="form-control" id="subject" name="subject" value="{{values.subject}}" placeholder="Leave blank to use the default subject">
</div>
<div class="form-group">
<label for="daterange">Time</label>
<div class="form-group-sm daterangeElm" style="position: relative">
<input type="text" id="daterange" class="form-control" value="">
<i class="glyphicon glyphicon-calendar fa fa-calendar" style="position: absolute; bottom: 10px; right: 24px; top: auto; cursor: pointer;"></i>
</div>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea class="form-control" name="text" value="{{values.text}}" rows="3">{{values.text}}</textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> Update</button>
</div>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function(event) {
$('#daterange').daterangepicker({
"showDropdowns": true,
"showISOWeekNumbers": true,
"timePicker": true,
"timePicker24Hour": true,
"autoApply": true,
"autoUpdateInput": false,
"locale": {
"direction": "ltr",
"format": "DD/MM/YYYY HH:mm",
"separator": " - ",
"applyLabel": "Select",
"cancelLabel": "Cancel",
"fromLabel": "From",
"toLabel": "To",
"customRangeLabel": "Custom",
"daysOfWeek": [
"Su",
"Mo",
"Tu",
"We",
"Th",
"Fr",
"Sa"
],
"monthNames": [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
],
"firstDay": 1
},
{{#if values.start}}
"startDate": moment("{{values.start}}").format('DD/MM/YYYY HH:mm'),
{{/if}}
{{#if values.end}}
"endDate": moment("{{values.end}}").format('DD/MM/YYYY HH:mm'),
{{/if}}
"alwaysShowCalendars": true
}, function(start, end, label) {
document.getElementById('start').value = start.valueOf();
document.getElementById('end').value = end.valueOf();
document.getElementById('daterange').value = start.format('DD/MM/YYYY HH:mm') + ' – ' + end.format('DD/MM/YYYY HH:mm');
});
$('.daterangeElm i').click(function() {
$(this).parent().find('input').click();
});
{{#if values.start}}
document.getElementById('daterange').value = moment("{{values.start}}").format('DD/MM/YYYY HH:mm') + ' – ' + moment("{{values.end}}").format('DD/MM/YYYY HH:mm');
{{/if}}
});
</script>

+ 140
- 0
webmail/views/account/create.hbs View File

@ -0,0 +1,140 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Create new account</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form method="post" id="create-form" action="/account/create">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" id="domain" name="domain" value="{{values.domain}}">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Account information</h3>
</div>
<div class="panel-body">
<p>
Enter your account details. Account username is allowed to include latin characters only. Activated accounts can add extra identity addresses that may contain unicode characters as well.
</p>
<div class="row">
<div class="col-md-6">
<div class="form-group{{#if errors.name}} has-error{{/if}}">
<label for="name">Your name</label>
<input type="text" class="form-control" name="name" id="name" placeholder="eg. &quot;Jaan Tamm&quot;" value="{{values.name}}" required>
{{#if errors.name}}
<span class="help-block">{{errors.name}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.username}} has-error{{/if}}">
<label for="name">Your new address (also the username)</label>
<div class="input-group">
<input type="text" class="form-control" name="username" id="username" placeholder="eg. &quot;username&quot; or &quot;user.name&quot;" value="{{values.username}}" pattern="^[A-Za-z0-9][A-Za-z\-\.0-9]*$" required>
<span class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@<span class="selected-domain" style="text-transform: lowercase;">{{values.domain}}</span><span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
{{#each domains}}
<li><a href="#" class="change-domain-link" data-domain="{{this}}">{{this}}</a></li>
{{/each}}
</ul>
</span>
</div>
{{#if errors.username}}
<span class="help-block">{{errors.username}}</span>
{{else}}
<span class="help-block">Latin letters and numbers only. Dots and dashes are allowed as separators.<span>
{{/if}}
</div>
<div class="form-group{{#if errors.password}} has-error{{/if}}">
<label for="password">Your password</label>
<input type="password" class="form-control" name="password" id="password" placeholder="eg. &quot;supersecret&quot;" required>
{{#if errors.password}}
<span class="help-block">{{errors.password}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.password}} has-error{{/if}}">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" name="password2" id="password2" placeholder="repeat password" required>
</div>
<div class="form-group{{#if errors.remember}} has-error{{/if}}">
<div class="checkbox">
<label>
<input type="checkbox" name="remember" required> Agree to <a href="/tos" target="_blank">terms of service</a>
{{#if errors.remember}}
<span class="help-block">{{errors.remember}}</span>
{{/if}}
</label>
</div>
</div>
</div>
</div>
<div class="form-group">
{{#if recaptcha}}
<button
class="g-recaptcha btn btn-success"
data-sitekey="{{recaptcha}}"
data-callback="onCreateSubmit">
<span class="glyphicon glyphicon-user" aria-hidden="true"></span>Create new account
</button>
{{else}}
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-user" aria-hidden="true"></span>Create new account</button>
{{/if}}
</div>
</div>
</div>
</form>
</div>
</div>
{{#if recaptcha}}
<script src='https://www.google.com/recaptcha/api.js'></script>
<script>
function onCreateSubmit(token) {
document.getElementById("create-form").submit();
}
</script>
{{/if}}
<script>
document.addEventListener('DOMContentLoaded', function() {
var domainElement = document.getElementById('domain');
var updateDomain = function(e,elm){
e.preventDefault();
var domain = elm.dataset.domain;
if(domain){
domainElement.value = domain;
document.querySelector('.selected-domain').textContent = domain;
}
};
var setupDomainButton = function(elm){
elm.addEventListener('click', function(e){updateDomain(e,elm)}, false);
elm.addEventListener('touch', function(e){updateDomain(e,elm)}, false);
}
var domainLinks = document.querySelectorAll('.change-domain-link');
for(var i=0, len = domainLinks.length; i<len; i++){
setupDomainButton(domainLinks[i]);
}
}, false);
</script>

+ 88
- 0
webmail/views/account/filters.hbs View File

@ -0,0 +1,88 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-filter" aria-hidden="true"></span> Filters</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Mail Filters</h3></div>
<div class="panel-body">
<p>Here you can create and modify filters that apply on all incoming messages.</p>
</div>
<table class="table table-responsive">
<tbody>
{{#if filters}}
{{#each filters}}
<tr>
<th>
{{index}}
</th>
<td>
<div class="pull-right">
<a href="/account/filters/edit?id={{id}}" class="btn btn-info btn-xs"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit</a>
<button type="button" data-filter="{{id}}" class="btn btn-danger btn-xs" data-toggle="modal" data-target="#deleteModal"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete</button>
</div>
<div>
Query: <strong>{{query}}</strong><br /> Action: {{action}}
</div>
</td>
</tr>
{{/each}}
{{else}}
<tr>
<td colspan="3">
There are no filters created
</td>
</tr>
{{/if}}
</tbody>
</table>
<div class="panel-body">
<div class="form-group">
<a href="/account/filters/create" class="btn btn-success"><span class="glyphicon glyphicon-filter" aria-hidden="true"></span> Add new filter</a>
</div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteModalLabel">Delete filter</h4>
</div>
<div class="modal-body">
Are you sure you want to permanently delete selected filter?
</div>
<div class="modal-footer">
<form method="post" action="/account/filters/delete">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" id="delete-form-filter" name="id" value="">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="submit" class="btn btn-danger bulk-delete-confirm">Yes, delete</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
$('#deleteModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var filter = button.data('filter'); // Extract info from data-* attributes
document.getElementById('delete-form-filter').value = filter;
});
}, false);
</script>

+ 18
- 0
webmail/views/account/filters/create.hbs View File

@ -0,0 +1,18 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create filter</h1>
</div>
</div>
<form method="post" action="/account/filters/create">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
{{> filter}}
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-filter" aria-hidden="true"></span> Create filter</button>
<a href="/account/filters" class="btn btn-warning"><span class="glyphicon glyphicon-menu-left" aria-hidden="true"></span> Cancel</a>
</div>
</form>

+ 18
- 0
webmail/views/account/filters/edit.hbs View File

@ -0,0 +1,18 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Updated filter</h1>
</div>
</div>
<form method="post" action="/account/filters/edit">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="id" value="{{values.id}}">
{{> filter}}
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-filter" aria-hidden="true"></span> Update filter</button>
<a href="/account/filters" class="btn btn-warning"><span class="glyphicon glyphicon-menu-left" aria-hidden="true"></span> Cancel</a>
</div>
</form>

+ 131
- 0
webmail/views/account/identities.hbs View File

@ -0,0 +1,131 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Account</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> accountmenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Manage identities</h3></div>
<div class="panel-body">
<p>Here you can add and modify alias addresses for your account. Aliases act just like your main address. You can not send out emails from identities that you do not own.</p>
</div>
<table class="table table-responsive">
<thead>
<th>
&nbsp;
</th>
<th>
Identity name
</th>
<th>
Alias Address
</th>
<th>
Created
</th>
<th>
&nbsp;
</th>
</thead>
<tbody>
{{#each identities}}
<tr class="{{#if main}}identity-main{{/if}}">
<th>
{{index}}
</th>
<td>
{{#if name}}
{{name}}
{{else}}
<em>–</em>
{{/if}}
</td>
<td>
{{#if main}}
{{address}} <span>(default)</span>
{{else}}
{{address}}
{{/if}}
</td>
<td class="datestring" title="{{created}}">
{{created}}
</td>
<td class="text-right">
{{#if ../canEdit}}
<a href="/account/identities/edit?id={{id}}" class="btn btn-info btn-xs"><span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit</a>
{{/if}}
<button class="btn btn-danger btn-xs" data-address="{{address}}" {{#if main}}disabled{{else}}data-identity="{{id}}" data-toggle="modal" data-target="#deleteModal"{{/if}}><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete</button>
</td>
</tr>
{{/each}}
</tbody>
</table>
<div class="panel-body">
<div class="form-group">
{{#if canCreate}}
<a href="/account/identities/create" class="btn btn-success"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> Add new address</a>
{{else}}
<p class="text-muted">
Maximum amount of identities created
</p>
{{/if}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteModalLabel">Delete address</h4>
</div>
<div class="modal-body">
Are you sure you want to permanently delete <strong id="delete-form-identity-val">this address</strong>?
</div>
<div class="modal-footer">
<form method="post" action="/account/identities/delete">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" id="delete-form-identity" name="id" value="">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="submit" class="btn btn-danger bulk-delete-confirm">Yes, delete</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
$('#deleteModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var identity = button.data('identity'); // Extract info from data-* attributes
document.getElementById('delete-form-identity').value = identity;
document.getElementById('delete-form-identity-val').textContent = button.data('address');
});
}, false);
</script>

+ 46
- 0
webmail/views/account/identities/create.hbs View File

@ -0,0 +1,46 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Account</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> accountmenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<form method="post" action="/account/identities/create">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" id="domain" name="domain" value="{{values.domain}}">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Identity information</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
{{> identity}}
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> Add new address</button>
<a href="/account/identities" class="btn btn-warning"><span class="glyphicon glyphicon-menu-left" aria-hidden="true"></span> Cancel</a>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

+ 46
- 0
webmail/views/account/identities/edit.hbs View File

@ -0,0 +1,46 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Account</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> accountmenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<form method="post" action="/account/identities/edit">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="id" value="{{values.id}}">
<input type="hidden" id="domain" name="domain" value="{{values.domain}}">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Identity information</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
{{> identity}}
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> Edit address</button>
<a href="/account/identities" class="btn btn-warning"><span class="glyphicon glyphicon-menu-left" aria-hidden="true"></span> Cancel</a>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

+ 103
- 0
webmail/views/account/index.hbs View File

@ -0,0 +1,103 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Account</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> accountmenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label class="control-label">Address</label>
<div class="input-group">
<p class="form-control-static">
<a href="mailto:{{address}}">{{address}}</a>
</p>
</div>
</div>
<div class="form-group ">
<div class="input-group ">
<button type="button" id="reg-proto" class="btn btn-primary btn-xs"><span class="glyphicon glyphicon-envelope" aria-hidden="true"></span> Register {{serviceName}} as default webmail handler</button>
</div>
</div>
<div class="form-group ">
<label class="control-label ">Quota</label>
<div class="input-group ">
<p class="form-control-static ">
Used <strong>{{storageUsed}}</strong> of <strong>{{quota}}</strong>
</p>
</div>
</div>
<div class="progress ">
<div class="progress-bar " role="progressbar " aria-valuenow=" {{storageOverview}} " aria-valuemin="0 " aria-valuemax="100 " style="min-width: 2em; ">
{{storageOverview}}%
</div>
</div>
<div class="form-group ">
<label class="control-label ">Messages sent</label>
<div class="input-group ">
<p class="form-control-static ">
Sent <strong>{{recipientsSent}}</strong> messages, daily allowed quota <strong>{{recipients}}</strong> messages
</p>
</div>
</div>
<div class="progress ">
<div class="progress-bar " role="progressbar " aria-valuenow=" {{recipientsOverview}} " aria-valuemin="0 " aria-valuemax="100 " style="min-width: 2em; ">
{{recipientsOverview}}%
</div>
</div>
<div class="form-group ">
<label class="control-label ">Forwarded messages</label>
<div class="input-group ">
<p class="form-control-static ">
Forwarded <strong>{{forwardsSent}}</strong> messages, daily allowed quota <strong>{{forwards}}</strong> messages
</p>
</div>
</div>
<div class="progress ">
<div class="progress-bar " role="progressbar " aria-valuenow=" {{forwardsOverview}} " aria-valuemin="0 " aria-valuemax="100 " style="min-width: 2em; ">
{{forwardsOverview}}%
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" id="service-name" value="{{serviceName}}" />
<script>
function registerMailClient(){
var origin = window.location.origin;
if (!origin) {
origin= window.location.protocol + '//' + window.location.hostname + (window.location.port ? (':' + window.location.port) : '');
}
navigator.registerProtocolHandler('mailto', origin + '/webmail/send?to=%s', document.getElementById('service-name').value);
}
document.getElementById('reg-proto').addEventListener('click', registerMailClient, false);
</script>

+ 68
- 0
webmail/views/account/login.hbs View File

@ -0,0 +1,68 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Log in</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form method="post" action="/account/login">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="_2faToken" id="_2faToken" value="">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Account information</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<div class="form-group{{#if errors.username}} has-error{{/if}}">
<label class="control-label" for="username">Username</label>
<input type="text" class="form-control lowercase" name="username" id="username" placeholder="username" value="{{values.username}}" required>
{{#if errors.username}}
<span class="help-block">{{errors.username}}{{#if errors.username_action}} – <a href="{{errors.username_action.target}}">{{errors.username_action.title}}</a>{{/if}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.password}} has-error{{/if}}">
<label for="password">Your password</label>
<input type="password" class="form-control" name="password" id="password" placeholder="v3rys3cret" required>
{{#if errors.password}}
<span class="help-block">{{errors.password}}</span>
{{/if}}
</div>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" name="remember"> Remember me
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-log-in" aria-hidden="true"></span> Log in</button>
</div>
</div>
</div>
</form>
</div>
</div>
<script src="/login-key-handler.js"></script>
<script>
loginKeyHandler.setup(
document.getElementById('username'),
document.getElementById('_2faToken'),
'2fa'
);
</script>

+ 52
- 0
webmail/views/account/login.hbs.new View File

@ -0,0 +1,52 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Log in <small>(Autoconfig with <a href="https://www.mozilla.org/thunderbird/" target="_blank">thunderbird</a>)</small></h1>
</div>
</div>
<div class="row">
<div class="col-md-6">
<form method="post" action="/account/login">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<fieldset>
<div class="form-group{{#if errors.username}} has-error{{/if}}">
<label class="control-label" for="username">Username</label>
<div class="input-group">
<input type="text" class="form-control lowercase" name="username" id="username" placeholder="username" value="{{values.username}}" pattern="^[A-Za-z0-9][A-Za-z\-\.0-9]*[A-Za-z0-9]$" required title="Valid email address user">
<span class="input-group-addon">@{{serviceDomain}}</span>
</div>
{{#if errors.username}}
<span class="help-block">{{errors.username}}{{#if errors.username_action}} – <a href="{{errors.username_action.target}}">{{errors.username_action.title}}</a>{{/if}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.password}} has-error{{/if}}">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" id="password" placeholder="v3rys3cret" required>
{{#if errors.password}}
<span class="help-block">{{errors.password}}</span>
{{/if}}
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" name="remember"> Remember me
</label>
</div>
</div>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-success">Log in</button>
</div>
</form>
</div>
</div>

+ 68
- 0
webmail/views/account/login.hbs.orig View File

@ -0,0 +1,68 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Log in</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form method="post" action="/account/login">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="_2faToken" id="_2faToken" value="">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Account information</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<div class="form-group{{#if errors.username}} has-error{{/if}}">
<label class="control-label" for="username">Username</label>
<input type="text" class="form-control lowercase" name="username" id="username" placeholder="username" value="{{values.username}}" required>
{{#if errors.username}}
<span class="help-block">{{errors.username}}{{#if errors.username_action}} – <a href="{{errors.username_action.target}}">{{errors.username_action.title}}</a>{{/if}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.password}} has-error{{/if}}">
<label for="password">Your password</label>
<input type="password" class="form-control" name="password" id="password" placeholder="v3rys3cret" required>
{{#if errors.password}}
<span class="help-block">{{errors.password}}</span>
{{/if}}
</div>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" name="remember"> Remember me
</label>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-log-in" aria-hidden="true"></span> Log in</button>
</div>
</div>
</div>
</form>
</div>
</div>
<script src="/login-key-handler.js"></script>
<script>
loginKeyHandler.setup(
document.getElementById('username'),
document.getElementById('_2faToken'),
'2fa'
);
</script>

+ 98
- 0
webmail/views/account/profile.hbs View File

@ -0,0 +1,98 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Account</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> accountmenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<form method="post" action="/account/profile">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<fieldset>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">General</h3>
</div>
<div class="panel-body">
<div class="form-group">
<label class="control-label">Username</label>
<div class="input-group">
<p class="form-control-static">{{values.username}}</p>
</div>
</div>
<div class="form-group{{#if errors.name}} has-error{{/if}}">
<label for="name">Your name</label>
<input type="text" class="form-control" name="name" id="name" placeholder="eg. &quot;Jaan Tamm&quot;" value="{{values.name}}">
{{#if errors.name}}
<span class="help-block">{{errors.name}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.spamLevel}} has-error{{/if}}">
<label for="name">Spam detection level</label>
<select class="form-control" name="spamLevel">
<option value="">
-- Select --
</option>
{{#each spamLevels}}
<option value="{{value}}" {{#if selected}}selected{{/if}}>
{{description}}
</option>
{{/each}}
</select>
{{#if errors.spamLevel}}
<span class="help-block">{{errors.spamLevel}}</span>
{{/if}}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Message forwarding</h3>
</div>
<div class="panel-body">
<p>
Leave the following fields blank if you do not wish to forward all incoming emails
</p>
<div class="form-group{{#if errors.targets}} has-error{{/if}}">
<label for="targets">Forward incoming messages to:</label>
<input type="text" class="form-control" name="targets" id="targets" placeholder="user@example.com" value="{{values.targets}}">
{{#if errors.targets}}
<span class="help-block">{{errors.targets}}</span>
{{/if}}
<span class="help-block">Use comma separated list of addresses for multiple recipients</span>
</div>
</div>
</div>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> Update</button>
</div>
</form>
</div>
</div>
</div>
</div>

+ 26
- 0
webmail/views/account/security.hbs View File

@ -0,0 +1,26 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Security</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> securitymenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<p>
Future feature
</p>
</div>
</div>
</div>
</div>

+ 131
- 0
webmail/views/account/security/2fa.hbs View File

@ -0,0 +1,131 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Security</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> securitymenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Two factor authentication</h3>
</div>
<div class="panel-body">
<p>
If two-factor authentication is enabled then you will be required to enter a code from an authenticator app when logging in.
TOTP compatible authenticator app like Google Authenticator is needed to use two-factor authentication.
</p>
<p>
<a href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1' style="display:inline-block;overflow:hidden;background:url(/images/en_badge_web_generic.png) no-repeat;width:135px;height:40px;background-size:contain;background-position: center;" target="_blank"></a>
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8" style="display:inline-block;overflow:hidden;background:url(//linkmaker.itunes.apple.com/assets/shared/badges/en-us/appstore-lrg.svg) no-repeat;width:135px;height:40px;background-size:contain;" target="_blank"></a>
</p>
<p>
External applications can not access IMAP, POP3 ja SMTP using the account password if two-factor authentication is enabled. <a href="/account/security/asps">Application specific passwords</a> must be generated instead for these applications.
</p>
</div>
<table class="table table-responsive">
<tr>
<td>
{{#if enabled2fa}}
Two factor authentication is <span class="label label-success"><span class="glyphicon glyphicon-qrcode" aria-hidden="true"></span> Enabled</span>
{{else}}
Two factor authentication is <span class="label label-default"><span class="glyphicon glyphicon-qrcode" aria-hidden="true"></span> Disabled</span>
{{/if}}
</td>
<td class="text-right">
{{#if enabled2fa}}
<button type="button" class="btn btn-danger btn-xs" data-toggle="modal" data-target="#deleteModal"><span class="glyphicon glyphicon-minus-sign" aria-hidden="true"></span> Disable</button>
{{else}}
<form method="post" id="enable-2fa" action="/account/security/2fa/enable-totp">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<button type="submit" class="btn btn-success btn-xs"><span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span> Enable</button>
</form>
{{/if}}
</td>
</tr>
{{#if enabled2fa}}
<tr>
<td>
{{#if enabledU2f}}
U2F security key is <span class="label label-success"><span class="glyphicon glyphicon-flash" aria-hidden="true"></span> Enabled</span>
{{else}}
U2F security key is <span class="label label-default"><span class="glyphicon glyphicon-flash" aria-hidden="true"></span> Disabled</span>
{{/if}}
</td>
<td class="text-right">
{{#if enabledU2f}}
<button type="button" class="btn btn-danger btn-xs" data-toggle="modal" data-target="#revokeModal"><span class="glyphicon glyphicon-minus-sign" aria-hidden="true"></span> Disable</button>
{{else}}
<form method="post" id="enable-u2f" action="/account/security/2fa/enable-u2f">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<button type="submit" class="btn btn-success btn-xs"><span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span> Enable</button>
</form>
{{/if}}
</td>
</tr>
{{/if}}
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteModalLabel">Disable 2FA</h4>
</div>
<div class="modal-body">
Are you sure you want to disable two factor authentication?
</div>
<div class="modal-footer">
<form method="post" action="/account/security/2fa/disable-totp">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="submit" class="btn btn-danger bulk-delete-confirm">Yes, disable</button>
</form>
</div>
</div>
</div>
</div>
<div class="modal" id="revokeModal" tabindex="-1" role="dialog" aria-labelledby="revokeModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="revokeModalLabel">Revoke key</h4>
</div>
<div class="modal-body">
Are you sure you want to revoke U2F security key?
</div>
<div class="modal-footer">
<form method="post" action="/account/security/2fa/disable-u2f">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="submit" class="btn btn-danger bulk-delete-confirm">Yes, revoke</button>
</form>
</div>
</div>
</div>
</div>

+ 35
- 0
webmail/views/account/security/asp.hbs View File

@ -0,0 +1,35 @@
<form method="post" id="generate-autoconfig" action="/account/autoconfig">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="password" value="{{password}}">
</form>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Application specific password</h3>
</div>
<div class="panel-body">
<p>
Use the generated password in external application for IMAP, POP3 or SMTP
</p>
<p>
<strong>{{description}}</strong>
</p>
<p class="lead bg-info text-center">
{{passwordFormatted}}
</p>
<p>
For OSX and iOS you can download configuration profile to auto-configure your email application
</p>
<p>
<div class="pull-right">
<a href="data:application/x-apple-aspen-config;base64,{{mobileconfig}}" download="{{user.username}}.mobileconfig" class="btn btn-info btn-xs"><span class="glyphicon glyphicon-cloud-download" aria-hidden="true"></span> OSX / iOS</a>
</div>
<a href="/account/security/asps"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> Go back</a>
</p>
</div>
</div>

+ 151
- 0
webmail/views/account/security/asps.hbs View File

@ -0,0 +1,151 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Security</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> securitymenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Application specific passwords</h3></div>
<div class="panel-body">
<p>Here are listed passwords generated for specific applications. If the password is leaked then delete it and generate a new one.</p>
<p>
Application Specific Passwords must be used for external applications if two factor authentication is enabled.
</p>
</div>
<table class="table table-responsive">
<thead>
<tr>
<th>
#
</th>
<th>
Description
</th>
<th>
Created
</th>
<th>
Used
</th>
<th>
&nbsp;
</th>
</tr>
</thead>
<tbody>
{{#if asps}}
{{#each asps}}
<tr>
<th>
{{index}}
</th>
<td>
{{description}}
</td>
<td class="datestring" title="{{created}}">
{{created}}
</td>
<td>
{{#if lastUse.time}}
<a href="/account/security/events?event={{lastUse.event}}"><span class="datestring" title="{{lastUse.time}}">{{lastUse.time}}</span></a>
{{else}}
never
{{/if}}
</td>
<td>
<div class="pull-right">
<button type="button" data-asp="{{id}}" class="btn btn-danger btn-xs" data-toggle="modal" data-target="#deleteModal"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete</button>
</div>
</td>
</tr>
{{/each}}
{{else}}
<tr>
<td colspan="4">
No application specific passwords generated
</td>
</tr>
{{/if}}
</tbody>
</table>
</div>
<form method="post" action="/account/security/asps/create">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<fieldset>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Create new application specific password</h3>
</div>
<div class="panel-body">
<div class="form-group{{#if errors.description}} has-error{{/if}}">
<label for="description">Application description</label>
<input type="text" class="form-control" name="description" id="description" placeholder="Password for Outlook ..." required>
{{#if errors.description}}
<span class="help-block">{{errors.description}}</span>
{{/if}}
</div>
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Generate password</button>
</div>
</div>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteModalLabel">Delete password</h4>
</div>
<div class="modal-body">
Are you sure you want to permanently delete Application Specific Password?
</div>
<div class="modal-footer">
<form method="post" action="/account/security/asps/delete">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" id="delete-form-asp" name="id" value="">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="submit" class="btn btn-danger bulk-delete-confirm">Yes, delete</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
$('#deleteModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var asp = button.data('asp'); // Extract info from data-* attributes
document.getElementById('delete-form-asp').value = asp;
});
}, false);
</script>

+ 34
- 0
webmail/views/account/security/enable-totp.hbs View File

@ -0,0 +1,34 @@
<form id="totp-form">
<input type="hidden" id="_csrf" value="{{csrfToken}}">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Two factor authentication</h3>
</div>
<div class="panel-body">
<p>
Scan the code with an authenticator app and enter resulting security code below to verify
</p>
<p class="lead text-center">
<img src="{{imageUrl}}" style="width: 200px;" width="200">
</p>
<div class="form-group" id="totp-token-field">
<label for="token">Security code</label>
<input type="number" class="form-control" id="token" placeholder="6 digit code" required autofocus>
<span class="help-block" id="totp-token-error" style="display: none"></span>
</div>
<div>
<div class="pull-right">
<button type="submit" id="totp-btn" class="btn btn-success" data-loading-text="Checking..."><span class="glyphicon glyphicon-ok" aria-hidden="true"></span> Verify</button>
</div>
<a href="/account/security"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> Cancel</a>
</div>
</div>
</div>
</form>
<script src="/enable-totp.js"></script>

+ 45
- 0
webmail/views/account/security/enable-u2f.hbs View File

@ -0,0 +1,45 @@
<input type="hidden" id="_csrf" value="{{csrfToken}}">
<input id="version" type="hidden" id="version" value="{{u2fRegRequest.version}}">
<input id="appId" type="hidden" id="appId" value="{{u2fRegRequest.appId}}">
<input id="challenge" type="hidden" id="challenge" value="{{u2fRegRequest.challenge}}">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Two factor authentication</h3>
</div>
<div class="panel-body">
<div style="margin:10px 0;">
<div id="u2f-wait">
<img src="/images/u2f-wait.png" />
</div>
<div id="u2f-fail" style="display: none">
<img src="/images/u2f-fail.png" />
</div>
<div id="u2f-success" style="display: none">
<img src="/images/u2f-success.png" />
</div>
</div>
<p id="message">
Initializing...
</p>
<div>
<a href="/account/security"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> Cancel</a>
</div>
</div>
</div>
<script>
// U2F support must be checked *before* loading /u2f-api.js
var U2FSUPPORT = typeof u2f === 'object' || typeof chrome === 'object';
</script>
<script src="/u2f-api.js"></script>
<script src="/enable-u2f.js"></script>

+ 115
- 0
webmail/views/account/security/events.hbs View File

@ -0,0 +1,115 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Security</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> securitymenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<table class="table table-responsive">
<thead>
<tr>
<th>
Environment
</th>
<th>
Action
</th>
<th>
Result
</th>
<th>
IP
</th>
<th>
Session
</th>
<th>
Time
</th>
</tr>
</thead>
<tbody>
{{#if results}}
{{#each results}}
<tr>
<td>
{{protocol}}
</td>
<td>
{{#if asp}}
<div class="pull-right">
<strong>{{asp.name}}</strong>
</div>
{{/if}}
{{action}}
({{events}})
</td>
<td>
{{#if label}}
<span class="label label-{{label}}">{{result}}</span>
{{else}}
{{result}}
{{/if}}
</td>
<td>
{{ip}}
</td>
<td>
{{#if sess}}
<em>{{sessStr}}</em>
{{else}}
{{/if}}
</td>
<td class="datestring-fixed text-right" title="{{created}}">
{{created}}
</td>
</tr>
{{/each}}
{{else}}
<tr>
<td colspan="6">
<em>No events found</em>
</td>
</tr>
{{/if}}
</tbody>
</table>
<nav aria-label="nav">
<ul class="pager">
{{#if previousCursor}}
<li class="previous"><a href="/account/security/events?previous={{previousCursor}}&amp;page={{previousPage}}"><span aria-hidden="true">&larr;</span> Newer</a></li>
{{else}}
<li class="previous disabled"><a href="#"><span aria-hidden="true">&larr;</span> Newer</a></li>
{{/if}}
{{#if nextCursor}}
<li class="next"><a href="/account/security/events?next={{nextCursor}}&amp;page={{nextPage}}">Older <span aria-hidden="true">&rarr;</span></a></li>
{{else}}
<li class="next disabled"><a href="#">Older <span aria-hidden="true">&rarr;</span></a></li>
{{/if}}
</ul>
</nav>
</div>
</div>
</div>
</div>

+ 98
- 0
webmail/views/account/security/gpg.hbs View File

@ -0,0 +1,98 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Security</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> securitymenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<form method="post" action="/account/security/gpg">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<fieldset>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">GPG Encryption</h3>
</div>
<div class="panel-body">
<p>
If encryption is enabled then all cleartext messages that are archived to this
account are encrypted using provided public key. Private key is not known to the
service so if they key is lost then messages can not be recovered. {{serviceName}}
is able to display encrypted messages if <a
href="https://www.mailvelope.com/">Mailvelope browser extension</a> is
installed, otherwise you would have to download the messages and open these in a
GPG-compatible email client.
</p>
<div class="form-group{{#if errors.encryptMessages}} has-error{{/if}}">
<label class="radio-inline">
<input type="radio" name="encryptMessages" id="encryptMessagesNo" value="false"
{{#unless values.encryptMessages}}checked{{/unless}}> Disable encryption
</label>
<label class="radio-inline">
<input type="radio" name="encryptMessages" id="encryptMessagesYes" value="true"
{{#if values.encryptMessages}}checked{{/if}}> Enable encryption
</label>
{{#if errors.encryptMessages}}
<span class="help-block">{{errors.encryptMessages}}</span>
{{/if}}
</div>
{{#if fingerprint}}
<div class="form-group">
<label>Current key:</label>
<div class="form-control-static">
<div class="pull-right">
<label>
<input type="checkbox" name="removeKey" value="yes" /> Remove current
key
</label>
</div>
<div>
<code class="response">{{fingerprint}}</code>
{{#if keyAddress}}(<em>{{keyAddress}}</em>){{/if}}
</div>
</div>
</div>
{{/if}}
<div class="form-group{{#if errors.pubKey}} has-error{{/if}}">
<label for="pubKey">GPG Public Key{{#if fingerprint}} (replaces current key){{/if}}:
</label>
<textarea class="form-control" style="font-family: monospace;" rows="6" id="pubKey"
name="pubKey"
placeholder="Begins with &#39;-----BEGIN PGP PUBLIC KEY BLOCK-----&#39;">{{pubKey}}</textarea>
{{#if errors.pubKey}}
<span class="help-block">{{errors.pubKey}}</span>
{{/if}}
<span class="help-block">Leave empty if you do not want to replace the current
key</span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-lock"
aria-hidden="true"></span> Update encryption settings</button>
</div>
</div>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>

+ 67
- 0
webmail/views/account/security/password.hbs View File

@ -0,0 +1,67 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Security</h1>
</div>
</div>
<div class="row">
<div class="col-md-12">
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
{{> securitymenu}}
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview">
<p>&nbsp;</p>
<form method="post" action="/account/security/password">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<fieldset>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Change Password</h3>
</div>
<div class="panel-body">
<p>
Change your account password here
</p>
<div class="form-group{{#if errors.existingPassword}} has-error{{/if}}">
<label for="existingPassword">Current password</label>
<input type="password" class="form-control" name="existingPassword" id="existingPassword" placeholder="eg. &quot;supersecret&quot;" autocomplete="off">
{{#if errors.existingPassword}}
<span class="help-block">{{errors.existingPassword}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.password}} has-error{{/if}}">
<label for="password">New password</label>
<input type="password" class="form-control" name="password" id="password" placeholder="eg. &quot;supersecret&quot;" autocomplete="off">
{{#if errors.password}}
<span class="help-block">{{errors.password}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.password}} has-error{{/if}}">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" name="password2" id="password2" placeholder="repeat password" autocomplete="off">
</div>
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> Change Password</button>
</div>
</div>
</div>
</fieldset>
</form>
</div>
</div>
</div>
</div>

+ 43
- 0
webmail/views/account/update-password.hbs View File

@ -0,0 +1,43 @@
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Change Password</h3>
</div>
<div class="panel-body">
<form method="post" action="/account/update-password">
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
<p>
Your password needs to be changed. Enter your new account password below
</p>
<div class="form-group{{#if errors.password}} has-error{{/if}}">
<label for="password">New password</label>
<input type="password" class="form-control" name="password" id="password" placeholder="eg. &quot;supersecret&quot;" autocomplete="off">
{{#if errors.password}}
<span class="help-block">{{errors.password}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.password2}} has-error{{/if}}">
<label for="password2">Repeat password</label>
<input type="password" class="form-control" name="password2" id="password2" placeholder="repeat password" autocomplete="off">
{{#if errors.password2}}
<span class="help-block">{{errors.password2}}</span>
{{/if}}
</div>
<div>
<div class="pull-right">
<button type="submit" id="totp-btn" class="btn btn-success"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> Change Password</button>
</div>
<a href="/account/logout"><span class="glyphicon glyphicon-arrow-left" aria-hidden="true"></span> Cancel</a>
</div>
<div class="clearfix"></div>
</form>
</div>
</div>

+ 7
- 0
webmail/views/error.hbs View File

@ -0,0 +1,7 @@
<h3>{{error.status}} Error</h3>
<p class="lead">{{message}}</p>
{{#if error.stack}}
<pre>{{error.stack}}</pre>
{{/if}}

+ 160
- 0
webmail/views/help.hbs View File

@ -0,0 +1,160 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Help</h1>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Account configuration</h3></div>
<div class="panel-body">
<p>
Use the following configuration for your desktop email client.
</p>
</div>
<table class="table table-responsive table-bordered">
<thead>
<tr>
<th>
&nbsp;
</th>
<th>
IMAP
</th>
<th>
POP3
</th>
<th>
SMTP
</th>
</tr>
</thead>
<tbody>
<tr>
<th>
Description
</th>
<td>
Access all messages and mailboxes
</td>
<td>
Access INBOX
</td>
<td>
Send messages
</td>
</tr>
{{#if user}}
<tr>
<th>
E-mail address
</th>
<td>
{{user.username}}@{{serviceDomain}}
</td>
<td>
{{user.username}}@{{serviceDomain}}
</td>
<td>
{{user.username}}@{{serviceDomain}}
</td>
</tr>
{{/if}}
<tr>
<th>
Server
</th>
<td>
{{setup.imap.hostname}}
</td>
<td>
{{setup.pop3.hostname}}
</td>
<td>
{{setup.smtp.hostname}}
</td>
</tr>
<tr>
<th>
Port
</th>
<td>
{{setup.imap.port}}
</td>
<td>
{{setup.pop3.port}}
</td>
<td>
{{setup.smtp.port}}
</td>
</tr>
<tr>
<th>
Security
</th>
<td>
{{#if setup.imap.secure}}
TLS/SSL
{{else}}
STARTTLS
{{/if}}
</td>
<td>
{{#if setup.pop3.secure}}
TLS/SSL
{{else}}
STARTTLS
{{/if}}
</td>
<td>
{{#if setup.smtp.secure}}
TLS/SSL
{{else}}
STARTTLS
{{/if}}
</td>
</tr>
<tr>
<th>
Username
</th>
{{#if user}}
<td>
{{user.username}}
</td>
<td>
{{user.username}}
</td>
<td>
{{user.username}}
</td>
{{else}}
<td>
Your username
</td>
<td>
Your username
</td>
<td>
Your username
</td>
{{/if}}
</tr>
<tr>
<th>
Password
</th>
<td>
********
</td>
<td>
********
</td>
<td>
********
</td>
</tr>
</tbody>
</table>
</div>

+ 3
- 0
webmail/views/index.hbs View File

@ -0,0 +1,3 @@
<script>
window.location.href = "https://webmail.{{DOMAIN}}/account/login";
</script>

+ 50
- 0
webmail/views/layout-popup.hbs View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{serviceName}} web client">
<meta name="author" content="Andris Reinman">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<title>{{serviceName}}
{{#if title}} | {{title}}{{/if}}
</title>
<!--<link rel="stylesheet" href="/bootstrap-3.3.7/css/bootstrap.min.css">-->
<link rel="stylesheet" href="/bootstrap-3.3.7/css/lumen.css">
<link href="/css/wildduck.css" rel="stylesheet">
<link href="/css/popup.css" rel="stylesheet">
</head>
<body>
<div class="flash-messages">
{{flash_messages}}
</div>
<div class="container">
<div class="form-popup">
{{{body}}}
</div>
</div>
<!-- /container -->
<footer class="footer">
<div class="container">
<p class="text-muted">&copy; 2019 <a href="/">{{serviceName}}</a>. <a href="mailto:info@{{serviceDomain}}">info@{{serviceDomain}}</a>. </p>
</div>
</footer>
{{> scripts}}
</body>
</html>

+ 67
- 0
webmail/views/layout-webmail.hbs View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
{{>header}}
</head>
<body>
{{>navbar}}
<div class="flash-messages">
{{flash_messages}}
</div>
<div class="webmail-container">
<div class="sidebar-container">
<div class="sidebar">
<div class="sidebar-logo">
<a href="/webmail">
<img src="/logo.png">
</a>
</div>
<div style="margin: 10px 0 10px 0;" class="text-center">
<a href="/webmail/send" style="width: 100%" class="btn btn-default"><span class="glyphicon glyphicon-edit" aria-hidden="true"></span> Compose message</a>
</div>
<ul class="nav nav-sidebar">
{{#each mailboxes}}
<li id="mailbox-list-{{id}}" {{#if selected}} class="active" {{/if}}>
<a href="/webmail/{{id}}">
<span class="badge pull-right unseen-counter-{{id}}" {{#if unseen}}style="display: block;"{{else}}style="display: none;"{{/if}}>{{unseen}}</span>
{{{prefix}}}
{{#if icon}}
<span class="glyphicon glyphicon-{{icon}}" aria-hidden="true"></span>
{{else}}
<span class="glyphicon glyphicon-triangle-right" aria-hidden="true"></span>
{{/if}}
<span>{{formatted}}</span> {{{suffix}}}
</a>
</li>
{{/each}}
<li style="margin-top: 20px;">
<a href="/webmail/create" class="text-muted"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create folder</a>
</li>
</ul>
</div>
</div>
<div class="webmail-main">
{{{body}}}
</div>
</div>
<footer class="footer">
<div class="container">
<p class="text-muted">&copy; 2019 <a href="/">{{serviceName}}</a>. <a href="mailto:info@{{serviceDomain}}">info@{{serviceDomain}}</a>. </p>
</div>
</footer>
{{> scripts}}
</body>
</html>

+ 30
- 0
webmail/views/layout.hbs View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
{{>header}}
</head>
<body>
{{>navbar}}
<div class="flash-messages">
{{flash_messages}}
</div>
<div class="container">
{{{body}}}
</div>
<footer class="footer">
<div class="container">
<p class="text-muted">&copy; 2019 <a href="/">{{serviceName}}</a>. <a href="mailto:info@{{serviceDomain}}">info@{{serviceDomain}}</a>. </p>
</div>
</footer>
{{> scripts}}
</body>
</html>

+ 3
- 0
webmail/views/partials/accountmenu.hbs View File

@ -0,0 +1,3 @@
<li role="presentation" class="{{#if accMenuOverview}}active{{/if}}"><a href="/account/">Overview</a></li>
<li role="presentation" class="{{#if accMenuProfile}}active{{/if}}"><a href="/account/profile">Profile</a></li>
<li role="presentation" class="{{#if accMenuIdentities}}active{{/if}}"><a href="/account/identities">Identities</a></li>

+ 150
- 0
webmail/views/partials/filter.hbs View File

@ -0,0 +1,150 @@
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Search messages by:</div>
<div class="panel-body">
<div class="form-group{{#if errors.query_from}} has-error{{/if}}">
<label for="query_from">From</label>
<input type="text" class="form-control input-sm" name="query_from" id="query_from" value="{{values.query_from}}">
{{#if errors.query_from}}
<span class="help-block">{{errors.query_from}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.query_to}} has-error{{/if}}">
<label for="query_to">To</label>
<input type="text" class="form-control input-sm" name="query_to" id="query_to" value="{{values.query_to}}">
{{#if errors.query_to}}
<span class="help-block">{{errors.query_to}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.query_subject}} has-error{{/if}}">
<label for="query_subject">Subject</label>
<input type="text" class="form-control input-sm" name="query_subject" id="query_subject" value="{{values.query_subject}}">
{{#if errors.query_subject}}
<span class="help-block">{{errors.query_subject}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.query_text}} has-error{{/if}}">
<label for="query_text">Includes the following text</label>
<input type="text" class="form-control input-sm" name="query_text" id="query_text" value="{{values.query_text}}">
{{#if errors.query_text}}
<span class="help-block">{{errors.query_text}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.query_listId}} has-error{{/if}}">
<label for="query_listId">List-ID</label>
<input type="text" class="form-control input-sm" name="query_listId" id="query_listId" value="{{values.query_listId}}">
{{#if errors.query_listId}}
<span class="help-block">{{errors.query_listId}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.query_ha}} has-error{{/if}}">
<label>Attachments</label>
<div>
<label class="checkbox-inline">
<input type="checkbox" name="query_haYes" id="query_haYes" value="true" {{#if values.query_haYes}}checked{{/if}}> Has attachments
</label>
<label class="checkbox-inline">
<input type="checkbox" name="query_haNo" id="query_haNo" value="true" {{#if values.query_haNo}}checked{{/if}}> Doesn't have attachments
</label>
</div>
{{#if errors.query_ha}}
<span class="help-block">{{errors.query_ha}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.query_size}} has-error{{/if}}">
<label for="query_sizeValue">Size</label>
<div class="form-inline">
<div class="form-group">
<span>Message size is</span>
<select class="form-control input-sm" id="query_sizeType" name="query_sizeType">
<option value="1" {{#if values.query_sizeTypeGt}}selected{{/if}}>greater than</option>
<option value="-1" {{#if values.query_sizeTypeLt}}selected{{/if}}>smaller than</option>
</select>
</div>
<div class="form-group">
<input type="number" class="form-control input-sm" id="query_sizeValue" name="query_sizeValue" value="{{values.query_sizeValue}}">
</div>
<div class="form-group">
<select class="form-control input-sm" id="query_sizeUnit" name="query_sizeUnit">
<option value="MB" {{#if values.query_sizeUnitMB}}selected{{/if}}>MB</option>
<option value="kB" {{#if values.query_sizeUnitKB}}selected{{/if}}>kB</option>
<option value="B" {{#if values.query_sizeUnitB}}selected{{/if}}>baiti</option>
</select>
</div>
</div>
{{#if errors.query_size}}
<span class="help-block">{{errors.query_size}}</span>
{{/if}}
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">When a message arrives that matches this search:</div>
<div class="panel-body">
<div class="checkbox">
<label>
<input type="checkbox" name="action_seenYes" id="action_seenYes" value="true" {{#if values.action_seenYes}}checked{{/if}}> Mark as seen
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="action_flagYes" id="action_flagYes" value="true" {{#if values.action_flagYes}}checked{{/if}}> Flag it
</label>
</div>
<div class="form-group{{#if errors.action_mailbox}} has-error{{/if}}">
<label for="action_mailbox">Move to mailbox:</label>
<select class="form-control input-sm" id="action_mailbox" name="action_mailbox">
<option value=""></option>
{{#each mailboxes}}
<option value="{{id}}" {{#if selected}}selected{{/if}}>{{path}}</option>
{{/each}}
</select>
{{#if errors.action_mailbox}}
<span class="help-block">{{errors.action_mailbox}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.action_targets}} has-error{{/if}}">
<label for="action_targets">Forward it to address:</label>
<input type="text" class="form-control input-sm" name="action_targets" id="action_targets" value="{{values.action_targets}}" placeholder="user@example.com">
<span class="help-block">Somma separated list of email addresses or URLs</span>
{{#if errors.action_targets}}
<span class="help-block">{{errors.action_targets}}</span>
{{/if}}
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="action_spamYes" id="action_spamYes" value="true" {{#if values.action_spamYes}}checked{{/if}}> Mark as spam
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="action_spamNo" id="action_spamNo" value="true" {{#if values.action_spamNo}}checked{{/if}}> Don't mark as spam
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="action_deleteYes" id="action_deleteYes" value="true" {{#if values.action_deleteYes}}checked{{/if}}> Delete it
</label>
</div>
</div>
</div>
</div>
</div>

+ 18
- 0
webmail/views/partials/header.hbs View File

@ -0,0 +1,18 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{serviceName}} web client">
<meta name="author" content="Andris Reinman">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<title>{{serviceName}}
{{#if title}} | {{title}}{{/if}}
</title>
<!--<link rel="stylesheet" href="/bootstrap-3.3.7/css/bootstrap.min.css">-->
<link rel="stylesheet" href="/bootstrap-3.3.7/css/lumen.css">
<link rel="stylesheet" href="/css/wildduck.css">

+ 68
- 0
webmail/views/partials/identity.hbs View File

@ -0,0 +1,68 @@
<div class="form-group{{#if errors.name}} has-error{{/if}}">
<label for="name">Name</label>
<input type="text" class="form-control" name="name" id="name" placeholder="eg &quot;John Smith&quot; or &quot;Accounting Department&quot;" value="{{#if values.name}}{{values.name}}{{/if}}" >
{{#if errors.name}}
<span class="help-block">{{errors.name}}</span>
{{else}}
<span class="help-block">This name is used as the sender name when using this identity. Keep blank to default to your account name</span>
{{/if}}
</div>
<div class="form-group{{#if errors.address}} has-error{{/if}}">
<label for="name">Alias address</label>
<div class="input-group">
<input type="text" class="form-control" name="address" id="address" placeholder="eg. &quot;username&quot; or &quot;user.name&quot; or &quot;андрис&quot;" value="{{values.address}}" required>
<span class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@<span class="selected-domain" style="text-transform: lowercase;">{{values.domain}}</span><span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
{{#each domains}}
<li><a href="#" class="change-domain-link" data-domain="{{this}}">{{this}}</a></li>
{{/each}}
</ul>
</span>
</div>
{{#if errors.address}}
<span class="help-block">{{errors.address}}</span>
{{else}}
<span class="help-block">Unicode characters are allowed in alias addresses.<span>
{{/if}}
</div>
{{#unless isMain}}
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" name="main" {{#if values.main}}checked{{/if}}> Set as default
</label>
</div>
</div>
{{/unless}}
<script>
document.addEventListener('DOMContentLoaded', function() {
var domainElement = document.getElementById('domain');
var updateDomain = function(e,elm){
e.preventDefault();
var domain = elm.dataset.domain;
if(domain){
domainElement.value = domain;
document.querySelector('.selected-domain').textContent = domain;
}
};
var setupDomainButton = function(elm){
elm.addEventListener('click', function(e){updateDomain(e,elm)}, false);
elm.addEventListener('touch', function(e){updateDomain(e,elm)}, false);
}
var domainLinks = document.querySelectorAll('.change-domain-link');
for(var i=0, len = domainLinks.length; i<len; i++){
setupDomainButton(domainLinks[i]);
}
}, false);
</script>

+ 44
- 0
webmail/views/partials/mailbox.hbs View File

@ -0,0 +1,44 @@
<fieldset>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Mailbox settings</h3>
</div>
<div class="panel-body">
{{#if isInbox}}
<div class="form-group">
<label for="name">Mailbox name</label>
<input type="text" class="form-control" name="name" id="name" placeholder="eg. &quot;Important Stuff&quot;" value="{{values.name}}" disabled>
<span class="help-block">INBOX folder can not be modified</span>
</div>
{{else}}
<div class="form-group{{#if errors.parent}} has-error{{/if}}">
<label for="parent">Mailbox Parent</label>
<select class="form-control" name="parent" id="parent">
<option value="">[No parent]</option>
{{#each parents}}
<option value="{{path}}" {{#if isParent}} selected {{/if}} >
{{name}}
</option>
{{/each}}
</select>
{{#if errors.parent}}
<span class="help-block">{{errors.parent}}</span>
{{/if}}
</div>
<div class="form-group{{#if errors.name}} has-error{{/if}}">
<label for="name">Mailbox name</label>
<input type="text" class="form-control" name="name" id="name" placeholder="eg. &quot;Important Stuff&quot;" value="{{values.name}}" required>
{{#if errors.name}}
<span class="help-block">{{errors.name}}</span>
{{/if}}
</div>
{{/if}}
</div>
</div>
</fieldset>

+ 51
- 0
webmail/views/partials/messagerow.hbs View File

@ -0,0 +1,51 @@
<tr id="msg_{{id}}" class="messagerow messagerow-{{mailbox}}-{{id}} {{#if seen}}message-seen{{else}}message-unseen{{/if}}">
<td class="messagerow-spacer"></td>
<td class="messagerow-checkbox">
<input type="checkbox" data-mailbox="{{mailbox}}" data-message="{{id}}" class="message-checkbox"/>
</td>
<td class="messagerow-star">
<a href="#" class="message-star {{#if flagged}}flagged{{else}}unflagged{{/if}}" data-mailbox="{{mailbox}}" data-message="{{id}}"><span class="glyphicon glyphicon-{{#if flagged}}star{{else}}star-empty{{/if}}" aria-hidden="true"></span></a>
</td>
<td class="messagerow-from">
<a href="/webmail/{{mailbox}}/message/{{id}}" class="messagerow-link">
{{{fromHtml}}}
</a>
</td>
<td class="messagerow-subject">
<a href="/webmail/{{mailbox}}/message/{{id}}" class="messagerow-link">
<span class="messagerow-subject-content">
{{#if mailboxName}}
<span class="label label-default">{{mailboxName}}</span>
{{/if}}
{{subject}}{{#if intro}} <span class="text-muted" style="font-weight: normal;">– {{intro}}</span>{{/if}}
</span>
</a>
</td>
<td class="messagerow-info">
<a href="/webmail/{{mailbox}}/message/{{id}}" class="messagerow-link">
{{#if encrypted}}
<span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
{{else}}
{{#if attachments}}
<span class="glyphicon glyphicon-paperclip" aria-hidden="true"></span>
{{/if}}
{{/if}}
</a>
</td>
<td class="messagerow-date">
<a href="/webmail/{{mailbox}}/message/{{id}}" class="messagerow-link">
<span class="datestring-fixed" title="{{date}}">
{{date}}
</span>
</a>
</td>
<td class="messagerow-spacer"></td>
</tr>

+ 85
- 0
webmail/views/partials/navbar.hbs View File

@ -0,0 +1,85 @@
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{#if user}}/webmail{{else}}/{{/if}}">
<img alt="{{serviceName}}" src="/favicon-32x32.png" width="20" height="20">
</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav navbar-right">
{{#if user}}
<li {{#if activeWebmail}} class="active" {{/if}}>
<a href="/webmail/">
<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> Webmail
<span class="badge pull-right unseen-counter-{{inboxId}}" {{#if inboxUnseen}}style="display: block;"{{else}}style="display: none;"{{/if}}>{{inboxUnseen}}</span>
</a>
</li>
<li {{#if activeFilters}} class="active" {{/if}}>
<a href="/account/filters">
<span class="glyphicon glyphicon-filter" aria-hidden="true"></span> Filters
</a>
</li>
<li {{#if activeAutoreply}} class="active" {{/if}}>
<a href="/account/autoreply">
<span class="glyphicon glyphicon-calendar" aria-hidden="true"></span> Autoreply
</a>
</li>
<li {{#if activeHelp}} class="active" {{/if}}>
<a href="/help">
<span class="glyphicon glyphicon glyphicon-question-sign" aria-hidden="true"></span> Help
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<img src="{{user.gravatar}}" class="profile-image img-circle" width="20" height="20">
{{#if user.name}}
{{user.name}}
{{else}}
{{user.username}}
{{/if}}
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li {{#if activeHome}} class="active" {{/if}}>
<a href="/account/">
<span class="glyphicon glyphicon-user" aria-hidden="true"></span> Account
</a>
</li>
<li {{#if activeSecurity}} class="active" {{/if}}>
<a href="/account/security">
<span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Security
</a>
</li>
<li role="separator" class="divider"></li>
<li><a href="/account/logout"><span class="glyphicon glyphicon-log-out" aria-hidden="true"></span> Log out</a></li>
</ul>
</li>
{{else}}
{{#if allowJoin}}
<li {{#if activeCreate}} class="active" {{/if}}>
<a href="/account/create">
<span class="glyphicon glyphicon-user" aria-hidden="true"></span> Create account
</a>
</li>
{{/if}}
<li {{#if activeLogin}} class="active" {{/if}}>
<a href="/account/login">
<span class="glyphicon glyphicon-log-in" aria-hidden="true"></span> Log in
</a>
</li>
{{/if}}
</ul>
</div>
</div>
</nav>

+ 71
- 0
webmail/views/partials/scripts.hbs View File

@ -0,0 +1,71 @@
<script src="/components/underscore/underscore-min.js"></script>
<script src="/components/jquery/dist/jquery.min.js"></script>
<script src="/components/promise-polyfill/dist/promise.min.js"></script>
<script src="/components/moment/min/moment-with-locales.min.js"></script>
<script src="/bootstrap-3.3.7/js/bootstrap.js"></script>
<link href="/components/bootstrap-daterangepicker/daterangepicker.css" rel="stylesheet">
<script src="/components/bootstrap-daterangepicker/daterangepicker.js"></script>
<link href="/components/summernote/dist/summernote.css" rel="stylesheet">
<script src="/components/summernote/dist/summernote.min.js"></script>
<script src="/components/fetch/fetch.js"></script>
<script src="/components/event-source-polyfill/src/eventsource.min.js"></script>
<script src="/components/handlebars/handlebars.min.js"></script>
<script src="/components/favico.js/favico.js"></script>
<script src="/wd.js"></script>
<script type="text/javascript">
$(function() {
$("[rel='tooltip']").tooltip();
});
</script>
{{#if inboxId}}
<script>
var INBOX_ID = '{{inboxId}}';
var INBOX_UNSEEN = {{inboxUnseen}};
var FAVICON = new Favico({
animation:'slide'
});
if(INBOX_UNSEEN){
FAVICON.badge(INBOX_UNSEEN);
}
</script>
{{else}}
<script>
var INBOX_ID = -1;
var INBOX_UNSEEN = 0;
var FAVICON = false;
</script>
{{/if}}
{{#if successlog}}
<script src="/login-key-handler.js"></script>
<script>
$(function() {
// store token about successful auth in this browser
var successlog = {{{successlog}}};
loginKeyHandler.set(successlog.username, successlog.value, 'recovery', successlog.days);
});
</script>
{{/if}}
<script>
window.setTimeout(function(){
var alerts = document.querySelectorAll('.flash-messages');
var elm;
for(var i=0, len = alerts.length; i<len; i++){
elm = alerts[i];
if(elm.parentNode){
elm.parentNode.removeChild(elm);
}
}
}, 10 * 1000);
</script>

+ 8
- 0
webmail/views/partials/searchfield.hbs View File

@ -0,0 +1,8 @@
<form method="get" action="/webmail/search">
<div class="input-group">
<input type="text" class="form-control input" name="query" placeholder="from:val to:val subject:val &quot;phrase&quot; word" value="{{query}}" required>
<span class="input-group-btn">
<button class="btn btn-default btn" type="submit"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></button>
</span>
</div>
</form>

+ 5
- 0
webmail/views/partials/securitymenu.hbs View File

@ -0,0 +1,5 @@
<li role="presentation" class="{{#if secMenu2fa}}active{{/if}}"><a href="/account/security/2fa">Two factor authentication</a></li>
<li role="presentation" class="{{#if secMenuPassword}}active{{/if}}"><a href="/account/security/password">Account Password</a></li>
<li role="presentation" class="{{#if secMenuAsps}}active{{/if}}"><a href="/account/security/asps">Application specific passwords</a></li>
<li role="presentation" class="{{#if secMenuGpg}}active{{/if}}"><a href="/account/security/gpg">GPG Encryption</a></li>
<li role="presentation" class="{{#if secMenuEvents}}active{{/if}}"><a href="/account/security/events">Security events</a></li>

+ 57
- 0
webmail/views/partials/tos.hbs View File

@ -0,0 +1,57 @@
<p>Last updated: January 24, 2018</p>
<p>Please read these Terms and Conditions ("Terms", "Terms and Conditions") carefully before using the http://{{serviceDomain}} website (the "Service") operated by {{serviceName}} ("us", "we", or "our").</p>
<p>Your access to and use of the Service is conditioned on your acceptance of and compliance with these Terms. These Terms apply to all visitors, users and others who access or use the Service.</p>
<p>By accessing or using the Service you agree to be bound by these Terms. If you disagree with any part of the terms then you may not access the Service. Terms and Conditions for {{serviceName}} based on the <a href="https://termsfeed.com/blog/sample-terms-and-conditions-template/">T&amp;C example from TermsFeed</a>.</p>
<h2>Accounts</h2>
<p>When you create an account with us, you must provide us information that is accurate, complete, and current at all times. Failure to do so constitutes a breach of the Terms, which may result in immediate termination of your account on our Service.</p>
<p>You are responsible for safeguarding the password that you use to access the Service and for any activities or actions under your password, whether your password is with our Service or a third-party service.</p>
<p>You agree not to disclose your password to any third party. You must notify us immediately upon becoming aware of any breach of security or unauthorized use of your account.</p>
<h2>Links To Other Web Sites</h2>
<p>Our Service may contain links to third-party web sites or services that are not owned or controlled by {{serviceName}}.</p>
<p>{{serviceName}} has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that {{serviceName}} shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such web sites or services.</p>
<p>We strongly advise you to read the terms and conditions and privacy policies of any third-party web sites or services that you visit.</p>
<h2>Termination</h2>
<p>We may terminate or suspend access to our Service immediately, without prior notice or liability, for any reason whatsoever, including without limitation if you breach the Terms.</p>
<p>All provisions of the Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability.</p>
<p>We may terminate or suspend your account immediately, without prior notice or liability, for any reason whatsoever, including without limitation if you breach the Terms.</p>
<p>Upon termination, your right to use the Service will immediately cease. If you wish to terminate your account, you may simply discontinue using the Service.</p>
<p>All provisions of the Terms which by their nature should survive termination shall survive termination, including, without limitation, ownership provisions, warranty disclaimers, indemnity and limitations of liability.</p>
<h2>Governing Law</h2>
<p>These Terms shall be governed and construed in accordance with the laws of Estonia, without regard to its conflict of law provisions.</p>
<p>Our failure to enforce any right or provision of these Terms will not be considered a waiver of those rights. If any provision of these Terms is held to be invalid or unenforceable by a court, the remaining provisions of these Terms will remain in effect. These Terms constitute the entire agreement between us regarding our Service, and supersede and replace any prior agreements we might have between us regarding the Service.</p>
<h2>Changes</h2>
<p>We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material we will try to provide at least 30 days notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.</p>
<p>By continuing to access or use our Service after those revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, please stop using the Service.</p>
<h2>Contact Us</h2>
<p>If you have any questions about these Terms, please contact us.</p>

+ 13
- 0
webmail/views/tos.hbs View File

@ -0,0 +1,13 @@
<div class="row">
<div class="col-md-12">
<h1><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Terms and Conditions ("Terms")</h1>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">TOS</h3></div>
<div class="panel-body">
{{>tos}}
</div>
</div>

+ 134
- 0
webmail/views/webmail/audit.hbs View File

@ -0,0 +1,134 @@
<h2 class="sub-header" style="display: flex;">
<div style="flex-grow: 1">
<table class="limited">
<tr class="messagerow-{{message.mailbox}}-{{messageData.id}}">
<td class="message-subject-line">
<span>{{messageData.subject}}</span>
</td>
</tr>
</table>
</div>
<div>
<a href="/webmail/{{mailbox.id}}/message/{{messageData.id}}" class="btn btn-default"><span class="glyphicon glyphicon-menu-left" aria-hidden="true"></span></a>
</div>
</h2>
<p>
Below are displayed timeline events related to the selected message. This includes receive info, forwarding and autoreplies
</p>
{{#each events}}
<dl class="dl-horizontal">
{{#if actionDescription}}
{{#if action}}
<dt>Action</dt>
<dd><strong><span class="text-{{actionLabel}}">{{actionDescription}}</span></strong></dd>
{{/if}}
{{else}}
{{#if action}}
<dt>Action</dt>
<dd><strong>{{action}}</strong></dd>
{{/if}}
{{/if}}
<dt>ID</dt>
<dd><strong>{{id}}{{#if seq}}.{{seq}}{{/if}}</strong></dd>
<dt>Time</dt>
<dd><span class="datestring-fixed" title="{{time}}">{{time}}</span></dd>
{{#if messageId}}
<dt>Message-ID</dt>
<dd>{{messageId}}</dd>
{{/if}}
{{#if from}}
<dt>From</dt>
<dd>{{from}}</dd>
{{/if}}
{{#if to}}
<dt>To</dt>
<dd>{{to}}</dd>
{{/if}}
{{#if targetList}}
<dt>{{#if toTitle}}{{toTitle}}{{else}}Forwarding{{/if}}</dt>
<dd>
{{#each targetList}}
<div><strong>{{../id}}.{{seq}}:</strong> {{text}} <code class="response">{{value}}</code></div>
{{/each}}
</dd>
{{/if}}
{{#if origin}}
<dt>Sending host</dt>
<dd>{{origin}}</dd>
{{/if}}
{{#if src}}
<dt>Local address</dt>
<dd>{{src}}</dd>
{{/if}}
{{#if mx}}
<dt>Destination</dt>
<dd>{{mx}}
{{#if dst}}
[{{dst}}]
{{/if}}
</dd>
{{/if}}
{{#if response}}
<dt>Server response</dt>
<dd><code class="response">{{response}}</code></dd>
{{/if}}
{{#if error}}
<dt>Error message</dt>
<dd><code class="response">{{error}}</code></dd>
{{/if}}
</dl>
{{/each}}
<p>
&nbsp;
</p>
<script>
document.addEventListener('DOMContentLoaded', function() {
var stream = new EventSource('/api/events');
stream.onmessage = function(e) {
var data, row, star, redrawTimer;
try {
data = JSON.parse(e.data);
} catch (E) {
return;
}
switch (data.command) {
case 'COUNTERS': {
if (data.mailbox) {
if(FAVICON && data.mailbox === INBOX_ID){
FAVICON.badge(data.unseen);
}
[].slice.call(document.querySelectorAll('.unseen-counter-' + data.mailbox)).forEach(function(row){
if(data.unseen){
row.style.display = 'block';
row.textContent = data.unseen;
}else {
row.style.display = 'none';
row.textContent = 0;
}
});
}
break;
}
}
};
});
</script>

+ 46
- 0
webmail/views/webmail/create.hbs View File

@ -0,0 +1,46 @@
<h2 class="sub-header"><span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> Create folder</h2>
<form method="post" action="/webmail/create">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
{{> mailbox}}
<div class="form-group">
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Create</button>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
var stream = new EventSource('/api/events');
stream.onmessage = function(e) {
var data, row, star, redrawTimer;
try {
data = JSON.parse(e.data);
} catch (E) {
return;
}
switch (data.command) {
case 'COUNTERS': {
if (data.mailbox) {
if(FAVICON && data.mailbox === INBOX_ID){
FAVICON.badge(data.unseen);
}
[].slice.call(document.querySelectorAll('.unseen-counter-' + data.mailbox)).forEach(function(row){
if(data.unseen){
row.style.display = 'block';
row.textContent = data.unseen;
}else {
row.style.display = 'none';
row.textContent = 0;
}
});
}
break;
}
}
};
});
</script>

+ 754
- 0
webmail/views/webmail/index.hbs View File

@ -0,0 +1,754 @@
<h2 class="sub-header">
{{#if mailbox.editable}}
<div class="pull-right">
<a href="/webmail/{{mailbox.id}}/settings" class="btn btn-default"><span class="glyphicon glyphicon-cog" aria-hidden="true"></span> Settings</a>
</div>
{{/if}}
{{#if mailbox.icon}}
<span class="glyphicon glyphicon-{{mailbox.icon}}" aria-hidden="true"></span>
{{else}}
<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span>
{{/if}}
{{mailbox.name}}
</h2>
<div class="toolbar-container">
<div class="toolbar-main">
<div class="pull-left" style="margin-left: 10px; width: 20px;">
<input type="checkbox" class="toggle-all" />
</div>
<fieldset id="action-toolbar" disabled>
<div class="form-group">
<button class="btn btn-default btn-xs bulk-mark-seen">Mark as Seen</button>
<button class="btn btn-default btn-xs bulk-mark-unseen">Mark as Unseen</button>
<span style="display: inline-block; width: 10px;"></span>
<button class="btn btn-default btn-xs bulk-delete" data-toggle="modal" data-target="#deleteModal"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete</button>
<div class="btn-group">
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> Move <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{{#each mailboxes}}
{{#if canMoveTo}}
<li><a href="#" class="bulk-move" data-mailbox="{{id}}" data-mailbox-path="{{path}}" data-toggle="modal" data-target="#moveModal">
{{{prefix}}}
{{#if icon}}
<span class="glyphicon glyphicon-{{icon}}" aria-hidden="true"></span>
{{else}}
<span class="glyphicon glyphicon-triangle-right" aria-hidden="true"></span>
{{/if}}
{{formatted}}
{{{suffix}}}</a></li>
{{/if}}
{{/each}}
</ul>
</div>
</div>
</fieldset>
</div>
<div class="toolbar-search">
{{>searchfield}}
</div>
</div>
<div class="clearfix"></div>
{{#if isTrash}}
<div class="alert alert-info" style="padding: 5px 15px;" role="alert">Messages in Trash folder are deleted permanently after 30 days</div>
{{/if}}
{{#if isJunk}}
<div class="alert alert-info" style="padding: 5px 15px;" role="alert">Messages in Junk Mail folder are deleted permanently after 30 days</div>
{{/if}}
<div class="table-responsive">
<table class="messagelist">
<colgroup>
<col class="messagerow-spacer-col" />
<col class="messagerow-checkbox-col" />
<col class="messagerow-star-col" />
<col class="messagerow-from-col" />
<col class="messagerow-subject-col" />
<col class="messagerow-info-col" />
<col class="messagerow-date-col" />
<col class="messagerow-spacer-col" />
</colgroup>
<tbody>
{{#each messages}}
{{>messagerow}}
{{/each}}
</tbody>
</table>
</div>
<nav aria-label="nav">
<ul class="pager">
{{#if previousCursor}}
<li class="previous"><a href="/webmail/{{mailbox.id}}?previous={{previousCursor}}&amp;page={{previousPage}}&amp;query={{query}}"><span aria-hidden="true">&larr;</span> Newer</a></li>
{{else}}
<li class="previous disabled"><a href="#"><span aria-hidden="true">&larr;</span> Newer</a></li>
{{/if}}
<li style="display: inline-block; padding-top: 7px;">
Page <strong>{{page}}</strong> (<strong>{{startStr}}</strong>–<strong>{{endStr}}</strong> out of <strong>{{resultsStr}}</strong> messages)
</li>
{{#if nextCursor}}
<li class="next"><a href="/webmail/{{mailbox.id}}?next={{nextCursor}}&amp;page={{nextPage}}&amp;query={{query}}">Older <span aria-hidden="true">&rarr;</span></a></li>
{{else}}
<li class="next disabled"><a href="#">Older <span aria-hidden="true">&rarr;</span></a></li>
{{/if}}
</ul>
</nav>
<!-- Modal -->
<div class="modal" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteModalLabel">Delete messages</h4>
</div>
<div class="modal-body">
{{#if skipTrash}}
Are you sure you want to permanently delete selected messages?
{{else}}
Are you sure you want to move selected messages to Trash folder?
{{/if}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="button" class="btn btn-danger bulk-delete-confirm" data-loading-text="Deleting..." >Yes, delete</button>
</div>
</div>
</div>
</div>
<div class="modal" id="moveModal" tabindex="-1" role="dialog" aria-labelledby="moveModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="moveModalLabel">Move messages</h4>
</div>
<div class="modal-body">
Are you sure you want to move selected messages to <span class="bulk-move-path">another folder</span>?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="button" class="btn btn-primary bulk-move-confirm" data-loading-text="Moving..." >Yes, move</button>
</div>
</div>
</div>
</div>
<script id="messagerow-template" type="text/x-handlebars-template">
{{{messageRowTemplate}}}
</script>
<input type="hidden" id="_csrf" value="{{csrfToken}}">
<input type="hidden" id="mailbox" value="{{mailbox.id}}">
<input type="hidden" id="cursor-type" value="{{cursorType}}">
<input type="hidden" id="cursor-value" value="{{cursorValue}}">
<input type="hidden" id="page" value="{{page}}">
<input type="hidden" id="mailbox-type" value="{{mailbox.specialUse}}">
<script>
// star toggle
(function(){
var toggleStar = function(e, elm){
e.preventDefault();
e.stopPropagation();
if(!elm || elm.dataset.status === 'pending'){
return;
}
elm.dataset.status = 'pending';
var flagged = elm.classList.contains('flagged');
var done = function(){
elm.dataset.status = 'done';
}
fetch('/api/toggle/flagged', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: elm.dataset.mailbox,
message: elm.dataset.message,
flagged: !flagged // toggle
})
})
.then(function(res) {
return res.json();
})
.then(function(res) {
if(res.error){
console.error(res.error);
return done();
}
if(flagged){
elm.classList.remove('flagged');
elm.classList.add('unflagged');
elm.querySelector('.glyphicon').classList.remove('glyphicon-star');
elm.querySelector('.glyphicon').classList.add('glyphicon-star-empty');
}else{
elm.classList.remove('unflagged');
elm.classList.add('flagged');
elm.querySelector('.glyphicon').classList.remove('glyphicon-star-empty');
elm.querySelector('.glyphicon').classList.add('glyphicon-star');
}
done();
}).catch(function(err){
console.error(err);
done();
});
};
var setupToggling = function(elm){
elm.addEventListener('click', function(e){
toggleStar(e, elm);
}, false);
}
var starElms = document.querySelectorAll('.message-star');
for(var i=0, len = starElms.length; i<len; i++){
setupToggling(starElms[i]);
}
})();
// checkboxes
document.addEventListener('DOMContentLoaded', function() {
var checkboxes = document.querySelectorAll('.message-checkbox');
var toolbarElm = document.querySelector('#action-toolbar');
var mailboxType = document.querySelector('#mailbox-type');
var toggleAllElm = document.querySelector('.toggle-all');
var isInbox = mailboxType === 'INBOX';
var isSent = mailboxType === '\\Sent';
var isTrash= mailboxType === '\\Trash';
var isJunk = mailboxType === '\\Junk';
var skipTrash = ['\\Trash', '\\Junk'].includes(mailboxType);
var getChecked = function(){
var result = [];
for(var i=0, len = checkboxes.length; i<len; i++){
if(checkboxes[i].checked){
result.push({
elm: checkboxes[i],
message: checkboxes[i].dataset.message,
mailbox: checkboxes[i].dataset.mailbox
});
}
}
return result;
}
var toggleToolbar = function(){
var checked = 0;
for(var i=0, len = checkboxes.length; i<len; i++){
if(checkboxes[i].checked){
checked++;
}
}
if(checked){
toolbarElm.disabled = false;
if(checked === checkboxes.length){
toggleAllElm.checked = true;
}
}else{
toolbarElm.disabled = true;
toggleAllElm.checked = false;
}
};
for(var i=0, len = checkboxes.length; i<len; i++){
checkboxes[i].addEventListener('click', toggleToolbar, false);
checkboxes[i].addEventListener('change', toggleToolbar, false);
}
var findRow = function(elm, level){
level = level || 0;
var parent = elm.parentNode;
if(!parent || level > 10){
return false;
}
if(parent.classList.contains('messagerow')){
return parent;
}
return findRow(parent, level+1);
}
var removeRow = function(id){
var row = document.getElementById('msg_' + id);
if(row && row.parentNode){
row.parentNode.removeChild(row);
}
}
var messagerowSource = document.getElementById('messagerow-template').innerHTML;
var messagerowTemplate = Handlebars.compile(messagerowSource);
// function to redraw email listing
var redrawList = function(done){
var checkedMessages = {};
getChecked().forEach(function(checked){
checkedMessages[checked.message] = true;
});
fetch('/api/list', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: document.getElementById('mailbox').value,
cursorType: document.getElementById('cursor-type').value,
cursorValue: document.getElementById('cursor-value').value,
page: document.getElementById('page').value
})
})
.then(function(res) {
return res.json();
})
.then(function(res) {
if(res.error){
console.error(res.error);
return done();
}
var html = res.results.map(function(message){
return messagerowTemplate(message);
}).join('\n');
document.querySelector('.messagelist tbody').innerHTML = html;
// reset page load handlers
checkboxes = document.querySelectorAll('.message-checkbox');
for(var i=0, len = checkboxes.length; i<len; i++){
if(checkedMessages[checkboxes[i].dataset.message]){
checkboxes[i].checked = true;
}
checkboxes[i].addEventListener('click', toggleToolbar, false);
checkboxes[i].addEventListener('change', toggleToolbar, false);
}
toggleToolbar();
$("[rel='tooltip']").tooltip();
updateFixedDatestrings()
done();
}).catch(function(err){
console.error(err);
done();
});
};
var pendingSeen = false;
var toggleSeen = function(seen){
if(pendingSeen){
return false;
}
var checked = getChecked();
if(!checked.length){
return false;
}
pendingSeen = true;
var groupkeys= [];
var groups = {};
checked.forEach(function(entry){
if(!groups[entry.mailbox]){
groups[entry.mailbox] = [];
groupkeys.push(entry.mailbox);
}
groups[entry.mailbox].push(entry.message);
})
var done = function(){
pendingSeen = false;
}
var batchPos = 0;
var processBatch = function(){
if(batchPos >= groupkeys.length){
return done();
}
var mailbox = groupkeys[batchPos++];
var messages = groups[mailbox];
fetch('/api/toggle/seen', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: mailbox,
message: messages.join(','),
seen: !!seen
})
})
.then(function(res) {
return res.json();
})
.then(function(res) {
if(res.error){
console.error(res.error);
return done();
}
checked.forEach(function(checkbox){
var row = findRow(checkbox.elm);
if(row){
row.classList.remove(seen ? 'message-unseen' : 'message-seen');
row.classList.add(seen ? 'message-seen' : 'message-unseen');
}
})
// continue processing
processBatch();
}).catch(function(err){
console.error(err);
done();
});
}
processBatch();
}
var setUnseen = function(){
toggleSeen(false);
};
var setSeen = function(){
toggleSeen(true);
};
document.querySelector('.bulk-mark-unseen').addEventListener('click', setUnseen, false);
document.querySelector('.bulk-mark-unseen').addEventListener('touch', setUnseen, false);
document.querySelector('.bulk-mark-seen').addEventListener('click', setSeen, false);
document.querySelector('.bulk-mark-seen').addEventListener('touch', setSeen, false);
var toggleAll = function(){
var checked = toggleAllElm.checked;
for(var i=0, len = checkboxes.length; i<len; i++){
checkboxes[i].checked = checked;
}
if(checked && checkboxes.length){
toolbarElm.disabled = false;
}
}
document.querySelector('.toggle-all').addEventListener('click', toggleAll, false);
document.querySelector('.toggle-all').addEventListener('change', toggleAll, false);
var pendingDeleted = false;
var deleteMessage = function(){
if(pendingDeleted){
return false;
}
var checked = getChecked();
if(!checked.length){
return false;
}
pendingDeleted = true;
$('#deleteModal .bulk-delete-confirm').button('loading');
var done = function(){
pendingDeleted = false;
$('#deleteModal .bulk-delete-confirm').button('reset');
$('#deleteModal').modal('hide');
}
var groupkeys= [];
var groups = {};
checked.forEach(function(entry){
if(!groups[entry.mailbox]){
groups[entry.mailbox] = [];
groupkeys.push(entry.mailbox);
}
groups[entry.mailbox].push(entry.message);
})
var deleted = 0;
var batchPos = 0;
var processBatch = function(){
if(batchPos >= groupkeys.length){
if(deleted){
return redrawList(done);
}
return done();
}
var mailbox = groupkeys[batchPos++];
var messages = groups[mailbox];
fetch('/api/delete', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: mailbox,
message: messages.join(',')
})
})
.then(function(res) {
return res.json();
})
.then(function(res) {
if(res.error){
console.error(res.error);
return done();
}
var removeRow = function(id){
var row = document.getElementById('msg_' + id);
if(row && row.parentNode){
row.parentNode.removeChild(row);
}
}
if(res.id && res.id.length){
for(var i=0, len = res.id.length; i<len; i++){
if(res.id[i] && res.id[i][0] && res.id[i][1]){
removeRow(res.id[i][0]);
deleted++;
}
}
}
processBatch();
}).catch(function(err){
console.error(err);
done();
});
}
processBatch();
};
document.querySelector('.bulk-delete-confirm').addEventListener('click', deleteMessage, false);
document.querySelector('.bulk-delete-confirm').addEventListener('touch', deleteMessage, false);
var pendingMove = false;
var moveMessage = function(target){
if(pendingMove){
return false;
}
var checked = getChecked();
if(!checked.length){
return false;
}
pendingMove = true;
$('#moveModal .bulk-move-confirm').button('loading');
var done = function(){
pendingMove = false;
$('#moveModal .bulk-move-confirm').button('reset');
$('#moveModal').modal('hide');
}
var targetMailbox = document.querySelector('.bulk-move-confirm').dataset.mailbox;
var groupkeys= [];
var groups = {};
checked.forEach(function(entry){
if(targetMailbox === entry.mailbox){
// skip
return;
}
if(!groups[entry.mailbox]){
groups[entry.mailbox] = [];
groupkeys.push(entry.mailbox);
}
groups[entry.mailbox].push(entry.message);
})
var moved = 0;
var batchPos = 0;
var processBatch = function(){
if(batchPos >= groupkeys.length){
if(moved){
return redrawList(done);
}
return done();
}
var mailbox = groupkeys[batchPos++];
var messages = groups[mailbox];
fetch('/api/move', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: mailbox,
message: messages.join(','),
target: targetMailbox
})
})
.then(function(res) {
return res.json();
})
.then(function(res) {
if(res.error){
console.error(res.error);
return done();
}
if(res.id && res.id.length){
for(var i=0, len = res.id.length; i<len; i++){
if(res.id[i] && res.id[i][0] && res.id[i][1]){
removeRow(res.id[i][0]);
moved++;
}
}
}
processBatch();
}).catch(function(err){
console.error(err);
done();
});
}
processBatch();
};
$('#moveModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var mailbox = button.data('mailbox'); // Extract info from data-* attributes
var path = button.data('mailbox-path');
$('.bulk-move-path').text(path);
document.querySelector('.bulk-move-confirm').dataset.mailbox = mailbox;
});
document.querySelector('.bulk-move-confirm').addEventListener('click', moveMessage, false);
document.querySelector('.bulk-move-confirm').addEventListener('touch', moveMessage, false);
let checkNewMessages = document.getElementById('page').value === '1' && document.getElementById('mailbox').value;
var stream = new EventSource('/api/events');
stream.onmessage = function(e) {
var data, row, star, redrawTimer;
try {
data = JSON.parse(e.data);
} catch (E) {
return;
}
switch (data.command) {
case 'EXISTS':
if(checkNewMessages === data.mailbox){
clearTimeout(redrawTimer);
redrawTimer = setTimeout(function(){
clearTimeout(redrawTimer);
redrawList(function(){});
}, 200);
}
break;
case 'FETCH': {
row = document.querySelector('.messagerow-'+data.mailbox+'-'+data.uid);
if(!row){
return;
}
if (data.flags) {
star = row.querySelector('.message-star');
if (data.flags.indexOf('\\Flagged')>=0) {
star.classList.remove('unflagged');
star.classList.add('flagged');
star.querySelector('.glyphicon').classList.remove('glyphicon-star-empty');
star.querySelector('.glyphicon').classList.add('glyphicon-star');
} else {
star.classList.remove('flagged');
star.classList.add('unflagged');
star.querySelector('.glyphicon').classList.remove('glyphicon-star');
star.querySelector('.glyphicon').classList.add('glyphicon-star-empty');
}
if (data.flags.indexOf('\\Seen')>=0) {
row.classList.remove('message-unseen');
row.classList.add('message-seen');
} else {
row.classList.remove('message-seen');
row.classList.add('message-unseen');
}
}
break;
}
case 'EXPUNGE': {
row = document.querySelector('.messagerow-'+data.mailbox+'-'+data.uid);
if(!row){
return;
}
if(row.parentNode){
row.parentNode.removeChild(row);
clearTimeout(redrawTimer);
redrawTimer = setTimeout(function(){
clearTimeout(redrawTimer);
redrawList(function(){});
}, 200);
}
}
case 'COUNTERS': {
if (data.mailbox) {
if(FAVICON && data.mailbox === INBOX_ID){
FAVICON.badge(data.unseen);
}
[].slice.call(document.querySelectorAll('.unseen-counter-' + data.mailbox)).forEach(function(row){
if(data.unseen){
row.style.display = 'block';
row.textContent = data.unseen;
}else {
row.style.display = 'none';
row.textContent = 0;
}
});
}
break;
}
}
};
toggleToolbar();
}, false);
</script>

+ 75
- 0
webmail/views/webmail/mailbox.hbs View File

@ -0,0 +1,75 @@
<h2 class="sub-header"><span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> {{mailbox.name}}</h2>
<form method="post" action="/webmail/{{mailbox.id}}/settings">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
{{> mailbox}}
{{#unless isInbox}}
<div class="form-group">
{{#unless isSpecial}}
<div class="pull-right">
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#deleteModal"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete</button>
</div>
{{/unless}}
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> Update</button>
</div>
{{/unless}}
</form>
<div class="modal" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteModalLabel">Delete folder</h4>
</div>
<div class="modal-body">
Are you sure you want to permanently delete <strong>{{mailbox.name}}</strong> and all its contents?
</div>
<div class="modal-footer">
<form method="post" action="/webmail/{{mailbox.id}}/delete">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="submit" class="btn btn-danger">Yes, delete</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var stream = new EventSource('/api/events');
stream.onmessage = function(e) {
var data, row, star, redrawTimer;
try {
data = JSON.parse(e.data);
} catch (E) {
return;
}
switch (data.command) {
case 'COUNTERS': {
if (data.mailbox) {
if(FAVICON && data.mailbox === INBOX_ID){
FAVICON.badge(data.unseen);
}
[].slice.call(document.querySelectorAll('.unseen-counter-' + data.mailbox)).forEach(function(row){
if(data.unseen){
row.style.display = 'block';
row.textContent = data.unseen;
}else {
row.style.display = 'none';
row.textContent = 0;
}
});
}
break;
}
}
};
});
</script>

+ 630
- 0
webmail/views/webmail/message.hbs View File

@ -0,0 +1,630 @@
<input type="hidden" id="mailbox" value="{{mailbox.id}}" />
<input type="hidden" id="message" value="{{message.id}}" />
<input type="hidden" id="_csrf" value="{{csrfToken}}">
<h2 class="sub-header" style="display: flex;">
<div style="flex-grow: 1">
<table class="limited">
<tr class="messagerow-{{message.mailbox}}-{{message.id}}">
<td class="message-subject-line">
<a href="#" class="message-star {{#if message.flagged}}flagged{{else}}unflagged{{/if}}"
data-mailbox="{{mailbox.id}}" data-message="{{message.id}}"><span
class="glyphicon glyphicon-{{#if message.flagged}}star{{else}}star-empty{{/if}}"
aria-hidden="true"></span></a>
<span>{{message.subject}}</span>
</td>
</tr>
</table>
</div>
<div>
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<span class="glyphicon glyphicon-envelope" aria-hidden="true"></span> Details <span
class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="/webmail/{{mailbox.id}}/raw/{{message.id}}.eml"><span
class="glyphicon glyphicon-download-alt" aria-hidden="true"></span> Original message</a>
</li>
{{#if message.attachments}}
<li role="separator" class="divider"></li>
{{#each message.attachments}}
<li><a href="/webmail/{{../mailbox.id}}/attachment/{{../message.id}}/{{id}}"
download="{{filename}}"><span class="glyphicon glyphicon-paperclip" aria-hidden="true"></span>
{{filename}} [{{sizeKb}}kB]</a></li>
{{/each}}
{{/if}}
</ul>
</div>
</div>
</h2>
<div class="toolbar-container">
<div class="toolbar-main">
<fieldset id="action-toolbar">
<div class="form-group">
<a href="/webmail/send?action=reply&amp;refMailbox={{mailbox.id}}&amp;refMessage={{message.id}}"
class="btn btn-default btn-xs"><span class="glyphicon glyphicon-send" aria-hidden="true"></span>
Reply</a>
<a href="/webmail/send?action=replyAll&amp;refMailbox={{mailbox.id}}&amp;refMessage={{message.id}}"
class="btn btn-default btn-xs"><span class="glyphicon glyphicon-send" aria-hidden="true"></span>
Reply to all</a>
<a href="/webmail/send?action=forward&amp;refMailbox={{mailbox.id}}&amp;refMessage={{message.id}}"
class="btn btn-default btn-xs"><span class="glyphicon glyphicon-share" aria-hidden="true"></span>
Forward</a>
<span style="display: inline-block; width: 10px;"></span>
<button class="btn btn-default btn-xs bulk-mark-unseen">Mark as Unseen</button>
<span style="display: inline-block; width: 10px;"></span>
<button class="btn btn-default btn-xs bulk-delete" data-toggle="modal" data-target="#deleteModal"><span
class="glyphicon glyphicon-trash" aria-hidden="true"></span> Delete</button>
<div class="btn-group">
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> Move <span
class="caret"></span>
</button>
<ul class="dropdown-menu">
{{#each mailboxes}}
{{#if canMoveTo}}
<li><a href="#" class="bulk-move" data-mailbox="{{id}}" data-mailbox-path="{{path}}"
data-toggle="modal" data-target="#moveModal">
{{{prefix}}}
{{#if icon}}
<span class="glyphicon glyphicon-{{icon}}" aria-hidden="true"></span>
{{else}}
<span class="glyphicon glyphicon-triangle-right" aria-hidden="true"></span>
{{/if}}
{{formatted}}
{{{suffix}}}</a></li>
{{/if}}
{{/each}}
</ul>
</div>
</div>
</fieldset>
</div>
<div class="toolbar-search">
{{>searchfield}}
</div>
</div>
<div class="clearfix"></div>
{{#each message.info}}
<div>
<strong>{{key}}:</strong>
{{#if icon}}
<span class="glyphicon glyphicon-{{icon}}" aria-hidden="true"></span>
{{/if}}
<span {{#if isDate}} class="datestring" title="{{value}}" {{/if}}>
{{#if isHtml}}{{{value}}}{{else}}{{value}}{{/if}}
</span>
{{#if @first}}
{{#if ../message.securityInfo}}
<a id="extraDetails" tabindex="0" role="button" data-toggle="popover" data-trigger="focus"
title="Delivery details"><span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span></a>
<div id="extraDetailsContent" style="display: none">
{{#each ../message.securityInfo}}
<div {{#if textClass}} class="{{textClass}}" {{/if}}>
<strong>{{key}}:</strong>
{{#if icon}}
<span class="glyphicon glyphicon-{{icon}}" aria-hidden="true"></span>
{{/if}}
<span {{#if isDate}} class="datestring" title="{{value}}" {{/if}}>
{{#if isHtml}}{{{value}}}{{else}}{{value}}{{/if}}
</span>
</div>
{{/each}}
</div>
{{/if}}
{{/if}}
</div>
{{/each}}
{{#if expires}}
<div class="text-muted">
<strong>Message expires:</strong>
<span class="datestring" title="{{expires}}">
{{expires}}
</span>
</div>
{{/if}}
<div style="margin-bottom: 5px;"></div>
{{#if message.encrypted}}
<div id="encrypted-warning" class="alert alert-warning" role="alert">
<span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
This message is encrypted and can not be displayed. Download the original message <a
href="/webmail/{{mailbox.id}}/raw/{{message.id}}.eml" download="{{message.id}}.eml" class="alert-link">from here
</a> to open it in an e-mail client that is able to read encrypted messages.
Alternatively you can install <a href="https://www.mailvelope.com/" class="alert-link">Mailvelope browser
extension</a> to allow decrypting and displaying messages by {{serviceName}}.
</div>
<div id="mailvelope-loading" class="alert alert-info" role="alert" style="display:none">
<span class="glyphicon glyphicon-lock" aria-hidden="true"></span>
Mailvelope detected. Trying to decrpyt encrypted message...
</div>
<div id="message-content" class="mailvelope"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
if (typeof mailvelope !== 'undefined') {
mailvelopeLoaded();
} else {
window.addEventListener('mailvelope', mailvelopeLoaded, false);
}
var identifier = 'wildduck';
function getKeyring(callback) {
mailvelope.getKeyring(identifier).then(function (keyring) {
return callback(null, keyring);
}).catch(function (err) {
mailvelope.createKeyring(identifier).then(function (keyring) {
return callback(null, keyring);
}).catch(function (err) {
return callback(err);
});
});
}
function mailvelopeLoaded() {
document.getElementById('encrypted-warning').style.display = 'none';
document.getElementById('mailvelope-loading').style.display = 'block';
var selector = '#message-content';
getKeyring(function (err, keyring) {
if (err) {
document.getElementById('encrypted-warning').style.display = 'block';
document.getElementById('mailvelope-loading').style.display = 'none';
alert('Failed to create keyring. ' + err.message);
return;
}
fetch('/webmail/{{mailbox.id}}/raw/{{message.id}}.eml', {
method: 'get',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include'
})
.then(function (res) {
return res.text();
})
.then(function (text) {
mailvelope.createDisplayContainer(selector, text, keyring, { showExternalContent: true }).then(function () {
//$(selector).addClass('mailvelope').find('.message-part, .part-notice').hide();
//setTimeout(function() { $(window).resize(); }, 10);
document.getElementById('mailvelope-loading').style.display = 'none';
}).catch(function (err) {
document.getElementById('encrypted-warning').style.display = 'block';
document.getElementById('mailvelope-loading').style.display = 'none';
console.error(err);
alert('Message decryption failed: ' + err.message, 'error')
});
}).catch(function (err) {
document.getElementById('encrypted-warning').style.display = 'block';
document.getElementById('mailvelope-loading').style.display = 'none';
console.error(err);
alert('Message decryption failed: ' + err.message)
});
});
}
});
</script>
{{else}}
<div id="message-content" class="iframe-box"></div>
{{#if message.attachments}}
<div class="well">
{{#each message.attachments}}
<a class="btn btn-success btn-sm" href="/webmail/{{../mailbox.id}}/attachment/{{../message.id}}/{{id}}"
role="button" download="{{filename}}"><span class="glyphicon glyphicon-cloud-download"
aria-hidden="true"></span> {{filename}}</a>
{{/each}}
</div>
{{/if}}
<p>
&nbsp;
</p>
<script type="text/javascript" src="/components/DOMPurify/dist/purify.min.js"></script>
<script>
var message = {{{ messageJson }}};
document.addEventListener("DOMContentLoaded", function (event) {
if (message.html) {
var clean = DOMPurify.sanitize(message.html.join('\n'), {
ALLOW_UNKNOWN_PROTOCOLS: true,
WHOLE_DOCUMENT: true,
FORBID_TAGS: ['form']
});
clean = clean.replace(/head>/, 'head><link rel="stylesheet" href="/css/mail.css" /><base target="_parent"><script>function resizeIframe(obj) {obj.style.height = obj.contentWindow.document.body.scrollHeight + "px";}</' + 'script>');
var iframe = document.createElement('iframe');
document.getElementById('message-content').appendChild(iframe);
iframe.contentWindow.document.open();
iframe.contentWindow.document.write(clean);
iframe.contentWindow.document.close();
iframe.contentWindow.addEventListener('load', function () {
iframe.contentWindow.resizeIframe(iframe);
});
iframe.contentWindow.document.addEventListener('DOMContentLoaded', function () {
iframe.contentWindow.resizeIframe(iframe);
});
}
}, false);
</script>
{{/if}}
<!-- Modal -->
<div class="modal" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteModalLabel">Delete message</h4>
</div>
<div class="modal-body">
{{#if isTrash}}
Are you sure you want to permanently delete this message?
{{else}}
Are you sure you want to move this message to Trash folder?
{{/if}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="button" class="btn btn-danger bulk-delete-confirm" data-loading-text="Deleting...">Yes,
delete</button>
</div>
</div>
</div>
</div>
<div class="modal" id="moveModal" tabindex="-1" role="dialog" aria-labelledby="moveModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="moveModalLabel">Move message</h4>
</div>
<div class="modal-body">
Are you sure you want to move this message to <span class="bulk-move-path">another folder</span>?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="button" class="btn btn-primary bulk-move-confirm" data-loading-text="Moving...">Yes,
move</button>
</div>
</div>
</div>
</div>
<script>
// star toggle
(function () {
var toggleStar = function (e, elm) {
e.preventDefault();
e.stopPropagation();
if (!elm || elm.dataset.status === 'pending') {
return;
}
elm.dataset.status = 'pending';
var flagged = elm.classList.contains('flagged');
var done = function () {
elm.dataset.status = 'done';
}
fetch('/api/toggle/flagged', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: elm.dataset.mailbox,
message: elm.dataset.message,
flagged: !flagged // toggle
})
})
.then(function (res) {
return res.json();
})
.then(function (res) {
if (res.error) {
console.error(res.error);
return done();
}
if (flagged) {
elm.classList.remove('flagged');
elm.classList.add('unflagged');
elm.querySelector('.glyphicon').classList.remove('glyphicon-star');
elm.querySelector('.glyphicon').classList.add('glyphicon-star-empty');
} else {
elm.classList.remove('unflagged');
elm.classList.add('flagged');
elm.querySelector('.glyphicon').classList.remove('glyphicon-star-empty');
elm.querySelector('.glyphicon').classList.add('glyphicon-star');
}
done();
}).catch(function (err) {
console.error(err);
done();
});
};
var setupToggling = function (elm) {
elm.addEventListener('click', function (e) {
toggleStar(e, elm);
}, false);
}
var starElms = document.querySelectorAll('.message-star');
for (var i = 0, len = starElms.length; i < len; i++) {
setupToggling(starElms[i]);
}
})();
// checkboxes
document.addEventListener('DOMContentLoaded', function () {
var toolbarElm = document.querySelector('#action-toolbar');
var mailbox = document.getElementById('mailbox').value;
var message = document.getElementById('message').value;
var pendingSeen = false;
var toggleSeen = function (seen) {
if (pendingSeen) {
return false;
}
var done = function () {
pendingSeen = false;
}
fetch('/api/toggle/seen', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: mailbox,
message: message,
seen: !!seen
})
})
.then(function (res) {
return res.json();
})
.then(function (res) {
if (res.error) {
console.error(res.error);
return done();
}
window.location.href = '/webmail/' + mailbox;
}).catch(function (err) {
console.error(err);
done();
});
}
var setUnseen = function () {
toggleSeen(false);
};
var setSeen = function () {
toggleSeen(true);
};
document.querySelector('.bulk-mark-unseen').addEventListener('click', setUnseen, false);
document.querySelector('.bulk-mark-unseen').addEventListener('touch', setUnseen, false);
var pendingDeleted = false;
var deleteMessage = function () {
if (pendingDeleted) {
return false;
}
pendingDeleted = true;
$('#deleteModal .bulk-delete-confirm').button('loading');
var done = function () {
pendingDeleted = false;
$('#deleteModal .bulk-delete-confirm').button('reset');
$('#deleteModal').modal('hide');
}
fetch('/api/delete', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: mailbox,
message: message
})
})
.then(function (res) {
return res.json();
})
.then(function (res) {
if (res.error) {
console.error(res.error);
return done();
}
window.location.href = '/webmail/' + mailbox;
}).catch(function (err) {
console.error(err);
done();
});
};
document.querySelector('.bulk-delete-confirm').addEventListener('click', deleteMessage, false);
document.querySelector('.bulk-delete-confirm').addEventListener('touch', deleteMessage, false);
var pendingMove = false;
var moveMessage = function (target) {
if (pendingMove) {
return false;
}
pendingMove = true;
$('#moveModal .bulk-move-confirm').button('loading');
var done = function () {
pendingMove = false;
$('#moveModal .bulk-move-confirm').button('reset');
$('#moveModal').modal('hide');
}
var targetMailbox = document.querySelector('.bulk-move-confirm').dataset.mailbox;
fetch('/api/move', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: mailbox,
message: message,
target: targetMailbox
})
})
.then(function (res) {
return res.json();
})
.then(function (res) {
if (res.error) {
console.error(res.error);
return done();
}
window.location.href = '/webmail/' + targetMailbox;
}).catch(function (err) {
console.error(err);
done();
});
};
$('#moveModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var mailbox = button.data('mailbox'); // Extract info from data-* attributes
var path = button.data('mailbox-path');
$('.bulk-move-path').text(path);
document.querySelector('.bulk-move-confirm').dataset.mailbox = mailbox;
});
document.querySelector('.bulk-move-confirm').addEventListener('click', moveMessage, false);
document.querySelector('.bulk-move-confirm').addEventListener('touch', moveMessage, false);
var stream = new EventSource('/api/events');
stream.onmessage = function (e) {
var data, row, star, redrawTimer;
try {
data = JSON.parse(e.data);
} catch (E) {
return;
}
switch (data.command) {
case 'FETCH': {
row = document.querySelector('.messagerow-' + data.mailbox + '-' + data.uid);
if (!row) {
return;
}
if (data.flags) {
star = row.querySelector('.message-star');
if (data.flags.indexOf('\\Flagged') >= 0) {
star.classList.remove('unflagged');
star.classList.add('flagged');
star.querySelector('.glyphicon').classList.remove('glyphicon-star-empty');
star.querySelector('.glyphicon').classList.add('glyphicon-star');
} else {
star.classList.remove('flagged');
star.classList.add('unflagged');
star.querySelector('.glyphicon').classList.remove('glyphicon-star');
star.querySelector('.glyphicon').classList.add('glyphicon-star-empty');
}
}
break;
}
case 'COUNTERS': {
if (data.mailbox) {
if (FAVICON && data.mailbox === INBOX_ID) {
FAVICON.badge(data.unseen);
}
[].slice.call(document.querySelectorAll('.unseen-counter-' + data.mailbox)).forEach(function (row) {
if (data.unseen) {
row.style.display = 'block';
row.textContent = data.unseen;
} else {
row.style.display = 'none';
row.textContent = 0;
}
});
}
break;
}
}
};
$('#extraDetails').popover({
content: document.getElementById('extraDetailsContent').innerHTML,
html: true
})
}, false);
</script>

+ 382
- 0
webmail/views/webmail/send.hbs View File

@ -0,0 +1,382 @@
<form method="post" id="send-form" class="form-horizontal" action="/webmail/send" enctype="multipart/form-data">
<input type="hidden" id="_csrf" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="action" value="{{values.action}}">
<input type="hidden" name="refMailbox" value="{{values.refMailbox}}">
<input type="hidden" name="refMessage" value="{{values.refMessage}}">
<input type="hidden" id="mailbox" name="draftMailbox" value="{{values.draftMailbox}}">
<input type="hidden" id="message" name="draftMessage" value="{{values.draftMessage}}">
<input type="hidden" name="draft" value="{{values.draft}}">
<div class="toolbar-container">
<div class="toolbar-main">
<fieldset id="action-toolbar">
<div class="form-group">
<div class="col-sm-offset-1 col-sm-11" style="margin-top: 20px;">
<button class="btn btn-primary btn-xs" type="button" data-toggle="modal" data-target="#sendModal"><span class="glyphicon glyphicon-send" aria-hidden="true"></span> Send message</button>
<button class="btn btn-default btn-xs" type="submit" name="userAction" value="save"><span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span> Save draft</button>
{{#if values.draft}}
<button class="btn btn-default btn-xs" type="button" data-toggle="modal" data-target="#deleteModal"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Discard Draft</button>
{{/if}}
</div>
</div>
</fieldset>
</div>
</div>
<div id="from-field" class="form-group{{#if errors.from}} has-error{{/if}}" {{#unless fromAddress}}style="display:none"{{/unless}}>
<label for="inputFrom" class="col-sm-1 control-label">From</label>
<div class="col-sm-11">
<select class="form-control" name="from" id="inputFrom">
{{#each addresses}}
<option value="{{id}}" {{#if selected}}selected{{/if}}>
{{#if name}}{{name}} – {{/if}} {{address}}
</option>
{{/each}}
</select>
{{#if errors.from}}
<span class="help-block">{{errors.from}}</span>
{{/if}}
</div>
</div>
<div class="form-group{{#if errors.to}} has-error{{/if}}">
<label for="inputTo" class="col-sm-1 control-label">To</label>
<div class="col-sm-11">
<input type="text" class="form-control" name="to" id="inputTo" value="{{values.to}}" placeholder="Recipient">
{{#if errors.to}}
<span class="help-block">{{errors.to}}</span>
{{/if}}
</div>
</div>
<div id="cc-field" class="form-group{{#if errors.cc}} has-error{{/if}}" {{#unless values.cc}}style="display:none"{{/unless}}>
<label for="inputCc" class="col-sm-1 control-label">Cc</label>
<div class="col-sm-11">
<input type="text" class="form-control" name="cc" id="inputCc" value="{{values.cc}}" placeholder="Cc">
{{#if errors.cc}}
<span class="help-block">{{errors.cc}}</span>
{{/if}}
</div>
</div>
<div id="bcc-field" class="form-group{{#if errors.bcc}} has-error{{/if}}" {{#unless values.bcc}}style="display:none"{{/unless}}>
<label for="inputBcc" class="col-sm-1 control-label">Bcc</label>
<div class="col-sm-11">
<input type="text" class="form-control" name="bcc" id="inputBcc" value="{{values.bcc}}" placeholder="Bcc">
{{#if errors.bcc}}
<span class="help-block">{{errors.bcc}}</span>
{{/if}}
</div>
</div>
<div class="text-right" style="margin-top: -10px; margin-bottom: 10px;">
<a href="#" id="link-add-from" {{#if fromAddress}}style="display:none"{{/if}}>From</a>
<a href="#" id="link-add-cc" {{#if values.cc}}style="display:none"{{/if}}>Cc</a>
<a href="#" id="link-add-bcc" {{#if values.bcc}}style="display:none"{{/if}}>Bcc</a>
</div>
<div class="form-group{{#if errors.subject}} has-error{{/if}}">
<label for="inputSubject" class="col-sm-1 control-label">Subject</label>
<div class="col-sm-11">
<input type="text" class="form-control" id="inputSubject" name="subject" value="{{values.subject}}" placeholder="Message subject">
{{#if errors.subject}}
<span class="help-block">{{errors.subject}}</span>
{{/if}}
</div>
</div>
<div class="form-group{{#if errors.editordata}} has-error{{/if}}">
<div class="col-sm-12">
<textarea id="summernote" name="editordata"></textarea>
{{#if errors.editordata}}
<span class="help-block">{{errors.editordata}}</span>
{{/if}}
</div>
</div>
<div id="attachment-field" class="form-group{{#if errors.attachment}} has-error{{/if}}">
<div class="col-sm-12">
<label for="inputAttachment">Attachments</label>
<input id="input-attachment" name="attachment" class="form-control file" type="file" multiple>
{{#if errors.attachment}}
<span class="help-block">{{errors.attachment}}</span>
{{/if}}
</div>
</div>
<!-- Modal -->
<div class="modal" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="deleteModalLabel">Delete draft</h4>
</div>
<div class="modal-body">
Are you sure you want to permanently delete this draft?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="button" class="btn btn-danger bulk-delete-confirm" data-loading-text="Deleting..." >Yes, delete</button>
</div>
</div>
</div>
</div>
<div class="modal" id="sendModal" tabindex="-1" role="dialog" aria-labelledby="sendModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="sendModalLabel">Send message</h4>
</div>
<div class="modal-body">
Are you sure you want to send this message?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">No, cancel</button>
<button type="submit" name="userAction" value="send" class="btn btn-primary bulk-send-confirm" data-loading-text="Sending..." >Yes, send</button>
</div>
</div>
</div>
</div>
</form>
<script type="text/javascript" src="/components/DOMPurify/dist/purify.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function(event) {
var messageHtml = {{{messageHtml}}};
if (messageHtml && messageHtml.length && /^\s*$/.test(document.getElementById('summernote').value)) {
// make sure that server timestamps get converted to browser time strings
// {%DATE ... %}
messageHtml = messageHtml.map(function(html){
return html.replace(/\{&DATE ([^&]+)&\}/g, function(m, d){
return moment(d.trim()).format('LLLL');
});
});
var clean = DOMPurify.sanitize(messageHtml.join('\n'), {
ALLOW_UNKNOWN_PROTOCOLS: true,
WHOLE_DOCUMENT: false,
FORBID_TAGS: ['form', 'style']
});
{{#unless keepHtmlAsIs}}
clean = '<br/><br/>\n<blockquote>' + clean + '</blockquote>';
{{/unless}}
document.getElementById('summernote').value = clean;
}
$('#summernote').summernote({
toolbar: [
// [groupName, [list of button]]
['style', ['bold', 'italic', 'underline', 'clear']],
['fontsize', ['fontsize']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']]
],
height: 300
});
var linkAddFrom = document.getElementById('link-add-from');
var linkAddCc = document.getElementById('link-add-cc');
var linkAddBcc = document.getElementById('link-add-bcc');
var showFrom = function(){
document.getElementById('from-field').style.display = 'block';
linkAddFrom.style.display = 'none';
document.getElementById('inputFrom').focus();
};
var showCc = function(){
document.getElementById('cc-field').style.display = 'block';
linkAddCc.style.display = 'none';
document.getElementById('inputCc').focus();
};
var showBcc = function(){
document.getElementById('bcc-field').style.display = 'block';
linkAddBcc.style.display = 'none';
document.getElementById('inputBcc').focus();
};
linkAddFrom.addEventListener('click', showFrom, false);
linkAddFrom.addEventListener('touch', showFrom, false);
linkAddCc.addEventListener('click', showCc, false);
linkAddCc.addEventListener('touch', showCc, false);
linkAddBcc.addEventListener('click', showBcc, false);
linkAddBcc.addEventListener('touch', showBcc, false);
}, false);
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var stream = new EventSource('/api/events');
stream.onmessage = function(e) {
var data, row, star, redrawTimer;
try {
data = JSON.parse(e.data);
} catch (E) {
return;
}
switch (data.command) {
case 'COUNTERS': {
if (data.mailbox) {
if(FAVICON && data.mailbox === INBOX_ID){
FAVICON.badge(data.unseen);
}
[].slice.call(document.querySelectorAll('.unseen-counter-' + data.mailbox)).forEach(function(row){
if(data.unseen){
row.style.display = 'block';
row.textContent = data.unseen;
}else {
row.style.display = 'none';
row.textContent = 0;
}
});
}
break;
}
}
};
});
</script>
<script>
// star toggle
(function(){
var toggleStar = function(e, elm){
e.preventDefault();
e.stopPropagation();
if(!elm || elm.dataset.status === 'pending'){
return;
}
elm.dataset.status = 'pending';
var flagged = elm.classList.contains('flagged');
var done = function(){
elm.dataset.status = 'done';
}
fetch('/api/toggle/flagged', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: elm.dataset.mailbox,
message: elm.dataset.message,
flagged: !flagged // toggle
})
})
.then(function(res) {
return res.json();
})
.then(function(res) {
if(res.error){
console.error(res.error);
return done();
}
if(flagged){
elm.classList.remove('flagged');
elm.classList.add('unflagged');
elm.querySelector('.glyphicon').classList.remove('glyphicon-star');
elm.querySelector('.glyphicon').classList.add('glyphicon-star-empty');
}else{
elm.classList.remove('unflagged');
elm.classList.add('flagged');
elm.querySelector('.glyphicon').classList.remove('glyphicon-star-empty');
elm.querySelector('.glyphicon').classList.add('glyphicon-star');
}
done();
}).catch(function(err){
console.error(err);
done();
});
};
var setupToggling = function(elm){
elm.addEventListener('click', function(e){
toggleStar(e, elm);
}, false);
}
var starElms = document.querySelectorAll('.message-star');
for(var i=0, len = starElms.length; i<len; i++){
setupToggling(starElms[i]);
}
})();
// toolbar buttons
document.addEventListener('DOMContentLoaded', function() {
var mailbox = document.getElementById('mailbox').value;
var message = document.getElementById('message').value;
var pendingDeleted = false;
var deleteMessage = function(){
if(pendingDeleted){
return false;
}
pendingDeleted = true;
$('#deleteModal .bulk-delete-confirm').button('loading');
var done = function(){
pendingDeleted = false;
$('#deleteModal .bulk-delete-confirm').button('reset');
$('#deleteModal').modal('hide');
}
fetch('/api/delete', {
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
_csrf: document.getElementById('_csrf').value,
mailbox: mailbox,
message: message
})
})
.then(function(res) {
return res.json();
})
.then(function(res) {
if(res.error){
console.error(res.error);
return done();
}
window.location.href = '/webmail/' + mailbox;
}).catch(function(err){
console.error(err);
done();
});
};
document.querySelector('.bulk-delete-confirm').addEventListener('click', deleteMessage, false);
document.querySelector('.bulk-delete-confirm').addEventListener('touch', deleteMessage, false);
}, false);
</script>

+ 24
- 0
wildduck/Dockerfile View File

@ -0,0 +1,24 @@
FROM node:10-slim
ARG DOMAIN
ARG REVERSE_DNS
RUN apt update && apt -y install git python make g++ libcap2-bin
RUN git clone https://github.com/nodemailer/wildduck /wildduck
RUN git clone https://github.com/zone-eu/zone-mta-template /wildduck-mta
RUN git clone https://github.com/haraka/Haraka /haraka
COPY ./haraka/config /haraka/config
COPY ./wildduck/config /wildduck/config
COPY ./wildduck-mta/config /wildduck-mta/config
RUN chown node.node -R /wildduck /wildduck-mta /haraka
RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/node
USER node
WORKDIR /wildduck
RUN npm install --production
WORKDIR /wildduck-mta
RUN npm install --production
RUN npm install zonemta-wildduck -S
RUN npm install zonemta-limiter -S
WORKDIR /haraka
RUN npm install
RUN npm install haraka-plugin-wildduck -S
RUN find ../haraka/config ../wildduck/config ../wildduck-mta/config -type f -exec sed -i "s/{{DOMAIN}}/$DOMAIN/g" {} +
RUN find ../haraka/config ../wildduck/config ../wildduck-mta/config -type f -exec sed -i "s/{{REVERSE_DNS}}/$REVERSE_DNS/g" {} +

+ 0
- 0
wildduck/haraka/attachments/.gitignore View File


+ 13
- 0
wildduck/haraka/config/access.domains View File

@ -0,0 +1,13 @@
# Basic whitelist/blacklist mechanism for domains and e-mail addresses
# add a single domain or e-mail per line
# default behavior for entries is to DENY or blacklist
# reverse behavior by prepending an exclamation point !
# foo.com <-- denied
# !foo.com <-- allowed
#
# More complex/granular behaviors are possible, e.g.
# To block everything claiming to be from aol.com, but still allow a single aol address:
# aol.com
# !friend@aol.com
#
# See full docs for details: http://haraka.github.io/manual/plugins/access.html

+ 6
- 0
wildduck/haraka/config/access.ini View File

@ -0,0 +1,6 @@
[check]
any=false
conn=true
helo=false
mail=true
rcpt=true

+ 14
- 0
wildduck/haraka/config/aliases View File

@ -0,0 +1,14 @@
{
"postmaster@{{DOMAIN}}": {
"action": "alias", "to": ["webmaster@{{DOMAIN}}"]
},
"info@{{DOMAIN}}": {
"action": "alias", "to": ["webmaster@{{DOMAIN}}"]
},
"admin@{{DOMAIN}}": {
"action": "alias", "to": ["webmaster@{{DOMAIN}}"]
},
"root@{{DOMAIN}}": {
"action": "alias", "to": ["webmaster@{{DOMAIN}}"]
}
}

+ 2
- 0
wildduck/haraka/config/attachment.ctype.regex View File

@ -0,0 +1,2 @@
executable
partial

+ 1
- 0
wildduck/haraka/config/attachment.filename.regex View File

@ -0,0 +1 @@
\.(?:ade|adp|bat|chm|cmd|com|cpl|dll|exe|hta|ins|isp|jar|js|jse|lib|lnk|mde|msc|msp|mst|pif|scr|sct|shb|sys|vb|vbe|vbs|vxd|wsc|wsf|wsh)$

+ 5
- 0
wildduck/haraka/config/auth_flat_file.ini View File

@ -0,0 +1,5 @@
[core]
methods=CRAM-MD5
[users]
; matt=test

+ 7
- 0
wildduck/haraka/config/auth_vpopmaild.ini View File

@ -0,0 +1,7 @@
host=127.0.0.6
port=89
;sysadmin=postmaster@example.com:sekret
[example.com]
host=127.0.0.10
;sysadmin=postmaster@example.com:sekret

+ 5
- 0
wildduck/haraka/config/avg.ini View File

@ -0,0 +1,5 @@
;host=
;port=54322
;tmpdir=/tmp
;connect_timeout=10
;session_timeout=30

+ 18
- 0
wildduck/haraka/config/bounce.ini View File

@ -0,0 +1,18 @@
; config/bounce_bad_rcpt: addresses that should never get bounces
[check]
single_recipient=true
empty_return_path=true
bad_rcpt=true
bounce_spf=true
non_local_msgid=true
; reject all bounce messages (generally a bad idea)
reject_all=false
[reject]
single_recipient=true
empty_return_path=true
bounce_spf=false
non_local_msgid=false

+ 5
- 0
wildduck/haraka/config/clamd.ini View File

@ -0,0 +1,5 @@
clamd_socket = /run/clamav/clamd.sock
[reject]
virus=true
error=false

+ 62
- 0
wildduck/haraka/config/data.headers.ini View File

@ -0,0 +1,62 @@
; configuration for data.headers plugin
; Requiring a date header will cause the loss of valid mail. The JavaMail
; sender used by some banks, photo processing services, health insurance
; companies, bounce senders, and others send messages without a Date header.
;
; If you can afford to reject some valid mail, please do enforce this, and
; encourage mailers toward RFC adherence. Otherwise, do not require Date.
; Headers that MUST be present (RFC 5322)
; required=From,Date ; <-- RFC 5322 compliant
required=From,Date
; Received
; If you have no outbound, add 'Received' to the required list for an
; aggressive anti-spam measure. It works because all real mail relays will
; add a `Received` header. It may false positive on some bulk mail that
; uses a custom tool to send, but this appears to be fairly rare.
; If the date header is present, and future and/or past days are
; defined, it will be validated. 0 = disabled
date_future_days=2
date_past_days=15
; Headers that MUST be unique if present (RFC 5322)
; singular=Date,From,Sender,Reply-To,To,Cc,Bcc,Message-Id,In-Reply-To,References,Subject (RFC 5322)
singular=Date,From,Sender,Reply-To,To,Cc,Bcc,Message-Id,In-Reply-To,References,Subject
; enable/disable the various header checks
[check]
; duplicate_singular=true
; missing_required=true
; invalid_return_path=true
; invalid_date=true
; user_agent=true
; direct_to_mx=true
; from_match=true
; mailing_list=true
; delivered_to=true
[reject]
; reject switches for each header check
; default are shown. Rejecting based on any of these
; criteria will result in the loss of valid mail.
;
; duplicate_singular=false
; missing_required=false
; invalid_return_path=false
; invalid_date=false
; arriving messages should not have Delivered-To set to the RCPT TO address.
; delivered_to=true
; these 4 do not have reject support, and likely shouldn't.
; user_agent=false
; direct_to_mx=false
; from_match=false
; from_match=true
; mailing_list=false

+ 202
- 0
wildduck/haraka/config/data.uribl.excludes View File

@ -0,0 +1,202 @@
# List derived from SpamAssassin
126.com
163.com
2o7.net
4at1.com
5iantlavalamp.com
about.com
adelphia.net
adobe.com
agora-inc.com
agoramedia.com
akamai.net
akamaitech.net
alexa.com
amazon.com
ancestry.com
aol.com
apache.org
apple.com
arcamax.com
ask.com
astrology.com
atdmt.com
att.net
baidu.com
bbc.co.uk
bcentral.com
bellsouth.net
bfi0.com
bing.com
bridgetrack.com
cafe24.com
charter.net
citibank.com
citizensbank.com
cjb.net
classmates.com
clickbank.net
cnet.com
cnn.com
comcast.net
com.com
com.ne.kr
corporate-ir.net
cox.net
craigslist.org
cs.com
custhelp.com
daum.net
dd.se
debian.org
dell.com
directnic.com
directtrack.com
div.tk
domain.com
doubleclick.com
dsbl.org
earthlink.net
ebay.com
ebay.co.uk
ebay.de
ebayimg.com
ebaystatic.com
edgesuite.net
ediets.com
egroups.com
emode.com
example.com
example.net
example.org
excite.com
facebook.com
fedex.com
flickr.com
freebsd.org
free.fr
f-secure.com
gentoo.org
geocities.com
gmail.com
gmx.net
go.com
godaddy.com
googleadservices.com
google.co.in
google.com
google.it
grisoft.com
hallmark.com
hinet.net
hotbar.com
hotmail.com
hotpop.com
hp.com
ibm.com
incredimail.com
investorplace.com
ivillage.com
joingevalia.com
juno.com
kernel.org
li.tk
livejournal.com
lycos.com
m7z.net
mac.com
macromedia.com
mail.com
mail.ru
mailscanner.info
marketwatch.com
mcafee.com
mchsi.com
messagelabs.com
microsoft.com
military.com
mindspring.com
mit.edu
monster.com
mozilla.com
msn.com
myspace.com
nate.com
netflix.com
netscape.com
netscape.net
netzero.net
norman.com
nytimes.com
openoffice.org
openxmlformats.org
optonline.net
osdn.com
overstock.com
pacbell.net
pandasoftware.com
passport.com
paypal.com
peoplepc.com
plaxo.com
prodigy.net
p.tk
radaruol.com.br
real.com
redhat.com
rediff.com
regions.com
regionsnet.com
rogers.com
rr.com
sbcglobal.net
sec.gov
sf.net
shaw.ca
shockwave.com
smithbarney.com
sourceforge.net
spamcop.net
speedera.net
sportsline.com
sun.com
suntrust.com
sympatico.ca
tails.nl
telus.net
terra.com.br
ticketmaster.com
tinyurl.com
tiscali.co.uk
tom.com
tone.co.nz
t-online.de
tux.org
twitter.com
uol.com.br
ups.com
usps.com
verizon.net
w3.org
wamu.com
wanadoo.fr
washingtonpost.com
weatherbug.com
web.de
webshots.com
webtv.net
wordpress.com
wsj.com
xmlsoap.org
yahoo.ca
yahoo.co.jp
yahoo.co.kr
yahoo.com
yahoo.com.br
yahoo.co.uk
yahoogroups.com
yimg.com
yopi.de
yoursite.com
youtube.com
zdnet.com

+ 37
- 0
wildduck/haraka/config/data.uribl.ini View File

@ -0,0 +1,37 @@
; If DBL not IPv6 compatible set:
; not_ipv6_compatible=1
[dbl.spamhaus.org]
validate=^(?!127\.0\.1\.255)127\.|(?!172\.255\.255\.255)
rdns=1
helo=1
envfrom=1
from=1
msgid=1
body=1
no_ip_lookups=1
custom_msg={uri} listed in {zone}; see http://www.spamhaus.org/query/dbl?domain={uri}
[multi.uribl.com]
validate=^127
strip_to_domain=1
; BLACK list only
bitmask=2
body=1
custom_msg={uri} listed in {zone}; see http://lookup.uribl.com/?domain={uri}
[multi.surbl.org]
validate=^127
strip_to_domain=1
body=1
;[fresh15.spameatingmonkey.net]
;validate=^127
;rdns=1
;helo=1
;envfrom=1
;from=1
;msgid=1
;body=1
;no_ip_lookups=1
;custom_msg={uri} domain registered within the last 15 days; see http://spameatingmonkey.com/lookup/{uri}

+ 1
- 0
wildduck/haraka/config/databytes View File

@ -0,0 +1 @@
26214400

+ 7
- 0
wildduck/haraka/config/delay_deny.ini View File

@ -0,0 +1,7 @@
; excluded plugins: a list of denials that are to be excluded (ie, all the immediate rejection)
; Examples: <plugin>
; <plugin>:<hook>
; <plugin>:<hook>:<function name>
;
;excluded_plugins=spf,lookup_rdns_strict

+ 8
- 0
wildduck/haraka/config/dhparams.pem View File

@ -0,0 +1,8 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEAojogVOvUcEffntS6DTp5zIMGWPJrFW8ZxZKIvSYUUlGD/QGWk8/T
CV6irXW7PrfGaOqn3DR+gHjwxoDHvz7tv5mBLvGgWDdEn4/4FNfdYIL3tC2E7Uaw
e2OwUCUgwWYh9Uytssrt0TXyjrAR54MEucU2ObS47m0sVkNNnRT1EfJU/LGC+Qtf
MVSL9FsLBZsexdQHJRXdUaInt/PclKgju0+D1gEzWBagqIPojukmuwl/kPSiV/qe
70By3wWp+fVZw5BXnXDKfQZ6Ox5nirNLPEZa4CaOEOfaTIsFhCBzn7wnLPWEp/Y+
VfnMbTRnRTP7HfrPw/MMCB7LYtVZU4JEUwIBAg==
-----END DH PARAMETERS-----

+ 5
- 0
wildduck/haraka/config/dkim_sign.ini View File

@ -0,0 +1,5 @@
disabled = false
selector = dkim
domain = {{DOMAIN}}
headers_to_sign = From, Sender, Reply-To, Subject, Date, Message-ID, To, Cc, MIME-Version
dkim.private.key = /secure/dkim.private

+ 23
- 0
wildduck/haraka/config/dnsbl.ini View File

@ -0,0 +1,23 @@
; reject: (default: true)
; denies connections from IPs on any active DNSBL
reject=true
; periodically check each DNSBL, disabling ones that fail checks
periodic_checks = 30
; search: Default (first)
; first: consider first DNSBL response conclusive. End processing.
; all: process all DNSBL results
search=first
; enable_stats (Default: false)
; stores stats in a Redis DB (see plugins/dns_list_base)
;enable_stats=true
; stats_redis_host (Default: localhost)
; zones: a comma separated list of DNSBL zones
; or list DNSBL zones in config/dnsbl.zones
zones=zen.spamhaus.org

+ 11
- 0
wildduck/haraka/config/early_talker.ini View File

@ -0,0 +1,11 @@
; delay in seconds
pause=5
; terminate the connection? (default: true)
; reject=false
; Whitelist of client IP ranges to skip delay on
[ip_whitelist]
::1
127.0.0.1

+ 14
- 0
wildduck/haraka/config/fcrdns.ini View File

@ -0,0 +1,14 @@
[reject]
; reject if the IP address has no PTR record
no_rdns=true
; reject if the FCrDNS test fails
no_fcrdns=true
; reject if the PTR points to a hostname without a valid TLD
invalid_tld=false
; reject if the rDNS is generic, examples:
; 1.2.3.4.in.addr.arpa
; c-67-171-0-90.hsd1.wa.comcast.net
generic_rdns=false

+ 43
- 0
wildduck/haraka/config/greylist.ini View File

@ -0,0 +1,43 @@
; Config for greylisting plugin
; greylisting action text
text = Greylisted. Please come back later.
[redis]
host = 127.0.0.1
; port = 6379
db = 11
[skip]
; skip for DNSWL hosts having high reputation
dnswlorg = true
mailspikewl = true
[period]
# transition path: first_connect --> black (defer) --> grey(allow) --> white (allow) --> expired
# 14 minutes
black = 850
# 25 hours
grey = 90000
# 35 days
white = 3024000
[envelope_whitelist]
# Envelope emails or domains, one per line
[ip_whitelist]
# IP or Subnet, one per line
[recipient_whitelist]
# Recipient emails or domains, one per line
[special_dynamic_domains]
# Put domains that should be always treated as dynamic here.
# Pattern is matched at the end of rdns
# SiteGround VPS service
sgvps.net

+ 57
- 0
wildduck/haraka/config/helo.checks.ini View File

@ -0,0 +1,57 @@
; disable checks or reject for each test if you are worried about strictness
;dns_timeout=30
[check]
; match_re=true
bare_ip=true
; dynamic=true
; big_company=true
; literal_mismatch: 1 = exact IP match, 2 = IP/24 match, 3 = /24 or RFC1918
; literal_mismatch=2
valid_hostname=true
forward_dns=true
rdns_match=true
; host_mismatch: hostname differs between EHLO invocations
host_mismatch=true
; proto_mismatch: host sent EHLO but then tries to sent HELO or vice-versa
proto_mismatch=true
[reject]
host_mismatch=true
; proto_mismatch=false
proto_mismatch=true
; rdns_match=false
rdns_match=true
; dynamic=false
; bare_ip=false
bare_ip=true
; literal_mismatch=false
; valid_hostname=false
valid_hostname=true
; forward_dns=false
forward_dns=true
; big_company=true
[skip]
; private_ip=true
; relaying=true
; whitelist=true ; TODO
[bigco]
msn.com=msn.com
hotmail.com=hotmail.com
yahoo.com=yahoo.com,yahoo.co.jp
yahoo.co.jp=yahoo.com,yahoo.co.jp
yahoo.co.uk=yahoo.co.uk
excite.com=excite.com,excitenetwork.com
mailexcite.com=excite.com,excitenetwork.com
yahoo.co.jp=yahoo.com,yahoo.co.jp
mailexcite.com=excite.com,excitenetwork.com
aol.com=aol.com
compuserve.com=compuserve.com,adelphia.net
nortelnetworks.com=nortelnetworks.com,nortel.com
earthlink.net=earthlink.net
earthling.net=earthling.net
google.com=google.com
gmail.com=google.com,gmail.com

+ 2
- 0
wildduck/haraka/config/host_list View File

@ -0,0 +1,2 @@
# add hosts in here we want to accept mail for
{{DOMAIN}}

+ 6
- 0
wildduck/haraka/config/host_list_regex View File

@ -0,0 +1,6 @@
# Add regexes in here we want to accept mail for.
# Specifies the list of regexes that are local to this server. Note
# all these regexes are anchored with ^regex$. One can not choose not to
# anchor with .* and that there is a good potential for bad regexes being
# over permissive if we don't do this.

+ 7
- 0
wildduck/haraka/config/http.ini View File

@ -0,0 +1,7 @@
; listen: the HTTP address:port(s) to listen on
; default: [::]:80 (port 80 on all IPv4 and IPv6 addresses)
; listen=[::]:80
; docroot: the directory where web content is served from
;docroot=/usr/local/haraka/html

+ 1
- 0
wildduck/haraka/config/internalcmd_key View File

@ -0,0 +1 @@
1d1336164e2210ed49371832271103fbc60a4bf6ab38c7ad07b25851290f19af

+ 7
- 0
wildduck/haraka/config/lmtp.ini View File

@ -0,0 +1,7 @@
;[main]
host=127.0.0.1
port=2424
; host=127.0.0.1
; [example.com]

+ 11
- 0
wildduck/haraka/config/log.ini View File

@ -0,0 +1,11 @@
[main]
; level=data, protocol, debug, info, notice, warn, error, crit, alert, emerg
level=info
; prepend timestamps to log entries? This setting does NOT affect logs emitted
; by logging plugins (like syslog).
timestamps=false
; format=default, logfmt
format=default

+ 12
- 0
wildduck/haraka/config/lookup_rdns.strict.ini View File

@ -0,0 +1,12 @@
[general]
nomatch=Please setup matching DNS and rDNS records.
timeout=60
timeout_msg=DNS check timed out.
[forward]
nxdomain=Please setup a forward DNS record.
dnserror=Please setup matching DNS and rDNS records.
[reverse]
nxdomain=Please setup a reverse DNS record.
dnserror=Please setup matching DNS and rDNS records.

+ 1
- 0
wildduck/haraka/config/lookup_rdns.strict.timeout View File

@ -0,0 +1 @@
0

+ 1
- 0
wildduck/haraka/config/lookup_rdns.strict.whitelist View File

@ -0,0 +1 @@
# Hostnames and IPs are matched exactly as written on each line.

+ 5
- 0
wildduck/haraka/config/lookup_rdns.strict.whitelist_regex View File

@ -0,0 +1,5 @@
# Does the same thing as the whitelist file, but each line is a regex.
# Each line is also anchored for you, meaning '^' + regex + '$' is added for
# you. If you need to get around this restriction, you may use a '.*' at
# either the start or the end of your regex. This should help prevent people
# from writing overly permissive rules on accident.

+ 4
- 0
wildduck/haraka/config/mail_from.is_resolvable.ini View File

@ -0,0 +1,4 @@
timeout=30
allow_mx_ip=0
reject_no_mx=1
re_bogus_ip=^(?:0\.0\.0\.0|255\.255\.255\.255|127\.)

+ 1
- 0
wildduck/haraka/config/max_unrecognized_commands View File

@ -0,0 +1 @@
10

+ 1
- 0
wildduck/haraka/config/me View File

@ -0,0 +1 @@
{{DOMAIN}}

+ 18
- 0
wildduck/haraka/config/messagesniffer.ini View File

@ -0,0 +1,18 @@
;port=9001
;tmpdir=/tmp
;gbudb_report_deny=true
;tag_string=[SPAM]
;[gbudb]
;white=accept
;caution=allow
;black=allow
;truncate=reject
;[message]
;white=allow
;local_white=accept
;caution=allow
;black=allow
;truncate=reject
;nonzero=reject

+ 30
- 0
wildduck/haraka/config/mongodb.ini View File

@ -0,0 +1,30 @@
; This file must be placed in "config" directory of your Haraka server.
;
; MongoDB Credentials
;
[mongodb]
; user
user=
; password
pass=
; host
host=mongo
; port
port=27017
; database name
db=haraka
; collection name
[collections]
queue=email_incoming_haraka
delivery=email_delivery_results
; Absolute path to store attachments
[attachments]
path=/home/node/Haraka/attachments
[enable]
queue=yes
delivery=yes

+ 15
- 0
wildduck/haraka/config/outbound.bounce_message View File

@ -0,0 +1,15 @@
Received: (Haraka {pid} invoked for bounce); {date}
Date: {date}
From: MAILER-DAEMON@{me}
To: {from}
Subject: failure notice
Message-Id: {msgid}
Hi. This is the Haraka Mailer program at {me}.
I'm afraid I wasn't able to deliver your message
"{subject}"
to the following addresses.
This is a permanent error; I've given up. Sorry it didn't work out.
Intended Recipients: {recipients}
Failure Reason: {reason}

+ 36
- 0
wildduck/haraka/config/outbound.bounce_message_html View File

@ -0,0 +1,36 @@
<html>
<head>
<style>
* {
font-family:Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
}
</style>
</head>
<body>
<table cellpadding="0" cellspacing="0" class="email-wrapper" style="padding-top:32px;background-color:#ffffff;"><tbody>
<tr><td>
<table cellpadding=0 cellspacing=0><tbody>
<tr><td style="max-width:560px;padding:24px 24px 32px;background-color:#fafafa;border:1px solid #e0e0e0;border-radius:2px">
<img style="padding:0 24px 16px 0;float:left" width=72 height=72 alt="Foutpictogram" src="cid:icon.png">
<table style="min-width:272px;padding-top:8px"><tbody>
<tr><td><h2 style="font-size:20px;color:#212121;font-weight:bold;margin:0">
Message not delivered
</h2></td></tr>
<tr><td style="padding-top:20px;color:#757575;font-size:16px;font-weight:normal;text-align:left">
A problem has occurred when trying to deliver your mail to <a style='color:#212121;text-decoration:none'><b>{recipients}</b></a> . Look below for the technical details.
</td></tr>
</tbody></table>
</td></tr>
</tbody></table>
</td></tr>
<tr style="border:none;background-color:#fff;font-size:12.8px;width:90%">
<td align="left" style="padding:48px 10px">
Reaction of the server: <br/>
<p style="font-family:monospace">
{reason}
</p>
</td>
</tr>
</tbody></table>
</body>
</html>

+ 106
- 0
wildduck/haraka/config/outbound.bounce_message_image View File

@ -0,0 +1,106 @@
Content-Type: image/png; name="icon.png"
Content-Disposition: attachment; filename="icon.png"
Content-Transfer-Encoding: base64
Content-ID: <icon.png>
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

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save