@ -0,0 +1,2 @@ | |||
DOMAIN=domain.com | |||
REVERSE_DNS=com.domain |
@ -0,0 +1 @@ | |||
secure |
@ -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 |
@ -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: |
@ -0,0 +1,7 @@ | |||
#!/bin/bash | |||
cd /haraka | |||
node haraka.js & | |||
cd /wildduck | |||
node server.js & | |||
cd /wildduck-mta | |||
npm start --production |
@ -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 |
@ -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" {} + |
@ -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 |
@ -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 |
@ -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> |
@ -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> |
@ -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. "Jaan Tamm"" 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. "username" or "user.name"" 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. "supersecret"" 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> |
@ -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">×</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> |
@ -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> |
@ -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> |
@ -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> </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> | |||
| |||
</th> | |||
<th> | |||
Identity name | |||
</th> | |||
<th> | |||
Alias Address | |||
</th> | |||
<th> | |||
Created | |||
</th> | |||
<th> | |||
| |||
</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">×</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> |
@ -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> </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> |
@ -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> </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> |
@ -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> </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> |
@ -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> |
@ -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> | |||
@ -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> |
@ -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> </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. "Jaan Tamm"" 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> |
@ -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> </p> | |||
<p> | |||
Future feature | |||
</p> | |||
</div> | |||
</div> | |||
</div> | |||
</div> |
@ -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> </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">×</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">×</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> |
@ -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> |
@ -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> </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> | |||
| |||
</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">×</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> |
@ -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> |
@ -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> |
@ -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> </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}}&page={{previousPage}}"><span aria-hidden="true">←</span> Newer</a></li> | |||
{{else}} | |||
<li class="previous disabled"><a href="#"><span aria-hidden="true">←</span> Newer</a></li> | |||
{{/if}} | |||
{{#if nextCursor}} | |||
<li class="next"><a href="/account/security/events?next={{nextCursor}}&page={{nextPage}}">Older <span aria-hidden="true">→</span></a></li> | |||
{{else}} | |||
<li class="next disabled"><a href="#">Older <span aria-hidden="true">→</span></a></li> | |||
{{/if}} | |||
</ul> | |||
</nav> | |||
</div> | |||
</div> | |||
</div> | |||
</div> |
@ -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> </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 '-----BEGIN PGP PUBLIC KEY BLOCK-----'">{{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> |
@ -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> </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. "supersecret"" 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. "supersecret"" 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> |
@ -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. "supersecret"" 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> |
@ -0,0 +1,7 @@ | |||
<h3>{{error.status}} Error</h3> | |||
<p class="lead">{{message}}</p> | |||
{{#if error.stack}} | |||
<pre>{{error.stack}}</pre> | |||
{{/if}} |
@ -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> | |||
| |||
</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> |
@ -0,0 +1,3 @@ | |||
<script> | |||
window.location.href = "https://webmail.{{DOMAIN}}/account/login"; | |||
</script> |
@ -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">© 2019 <a href="/">{{serviceName}}</a>. <a href="mailto:info@{{serviceDomain}}">info@{{serviceDomain}}</a>. </p> | |||
</div> | |||
</footer> | |||
{{> scripts}} | |||
</body> | |||
</html> |
@ -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">© 2019 <a href="/">{{serviceName}}</a>. <a href="mailto:info@{{serviceDomain}}">info@{{serviceDomain}}</a>. </p> | |||
</div> | |||
</footer> | |||
{{> scripts}} | |||
</body> | |||
</html> |
@ -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">© 2019 <a href="/">{{serviceName}}</a>. <a href="mailto:info@{{serviceDomain}}">info@{{serviceDomain}}</a>. </p> | |||
</div> | |||
</footer> | |||
{{> scripts}} | |||
</body> | |||
</html> |
@ -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> |
@ -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> |
@ -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"> |
@ -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 "John Smith" or "Accounting Department"" 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. "username" or "user.name" or "андрис"" 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> |
@ -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. "Important Stuff"" 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. "Important Stuff"" value="{{values.name}}" required> | |||
{{#if errors.name}} | |||
<span class="help-block">{{errors.name}}</span> | |||
{{/if}} | |||
</div> | |||
{{/if}} | |||
</div> | |||
</div> | |||
</fieldset> |
@ -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> |
@ -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> |
@ -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> |
@ -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 "phrase" 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> |
@ -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> |
@ -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&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> |
@ -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> |
@ -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> | |||
| |||
</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> |
@ -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> |
@ -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}}&page={{previousPage}}&query={{query}}"><span aria-hidden="true">←</span> Newer</a></li> | |||
{{else}} | |||
<li class="previous disabled"><a href="#"><span aria-hidden="true">←</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}}&page={{nextPage}}&query={{query}}">Older <span aria-hidden="true">→</span></a></li> | |||
{{else}} | |||
<li class="next disabled"><a href="#">Older <span aria-hidden="true">→</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">×</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">×</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> |
@ -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">×</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> |
@ -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&refMailbox={{mailbox.id}}&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&refMailbox={{mailbox.id}}&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&refMailbox={{mailbox.id}}&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> | |||
| |||
</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">×</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">×</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> |
@ -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">×</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">×</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> |
@ -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 +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 |
@ -0,0 +1,6 @@ | |||
[check] | |||
any=false | |||
conn=true | |||
helo=false | |||
mail=true | |||
rcpt=true |
@ -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}}"] | |||
} | |||
} |
@ -0,0 +1,2 @@ | |||
executable | |||
partial |
@ -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)$ |
@ -0,0 +1,5 @@ | |||
[core] | |||
methods=CRAM-MD5 | |||
[users] | |||
; matt=test |
@ -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 |
@ -0,0 +1,5 @@ | |||
;host= | |||
;port=54322 | |||
;tmpdir=/tmp | |||
;connect_timeout=10 | |||
;session_timeout=30 |
@ -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 |
@ -0,0 +1,5 @@ | |||
clamd_socket = /run/clamav/clamd.sock | |||
[reject] | |||
virus=true | |||
error=false |
@ -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 | |||
@ -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 |
@ -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} |
@ -0,0 +1 @@ | |||
26214400 |
@ -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 |
@ -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----- |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -0,0 +1,2 @@ | |||
# add hosts in here we want to accept mail for | |||
{{DOMAIN}} |
@ -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. | |||
@ -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 |
@ -0,0 +1 @@ | |||
1d1336164e2210ed49371832271103fbc60a4bf6ab38c7ad07b25851290f19af |
@ -0,0 +1,7 @@ | |||
;[main] | |||
host=127.0.0.1 | |||
port=2424 | |||
; host=127.0.0.1 | |||
; [example.com] |
@ -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 |
@ -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. |
@ -0,0 +1 @@ | |||
0 |
@ -0,0 +1 @@ | |||
# Hostnames and IPs are matched exactly as written on each line. |
@ -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. |
@ -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\.) |
@ -0,0 +1 @@ | |||
10 |
@ -0,0 +1 @@ | |||
{{DOMAIN}} |
@ -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 |
@ -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 |
@ -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} |
@ -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> |
@ -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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 |