mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-15 17:37:22 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65861c68b7 | ||
|
|
71a7a3e201 | ||
|
|
1c3bca53bb | ||
|
|
b643cb3478 |
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,17 +1,13 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
In Development
|
||||
--------------
|
||||
Version 55 (October 18, 2021)
|
||||
-----------------------------
|
||||
|
||||
Mail:
|
||||
|
||||
* "SMTPUTF8" is now disabled in Postfix. Because Dovecot still does not support SMTPUTF8, incoming mail to internationalized addresses was bouncing. This fixes incoming mail to internationalized domains (which was probably working prior to v0.40), but it will prevent sending outbound mail to addresses with internationalized local-parts.
|
||||
* Upgraded to Roundcube 1.5 Release Candidate.
|
||||
|
||||
Firewall:
|
||||
|
||||
* Fail2ban's IPv6 support is enabled.
|
||||
* Upgraded to Roundcube 1.5.
|
||||
|
||||
Control panel:
|
||||
|
||||
@@ -27,6 +23,7 @@ Control panel:
|
||||
|
||||
Other:
|
||||
|
||||
* Fail2ban's IPv6 support is enabled.
|
||||
* The mail log tool now doesn't crash if there are email addresess in log messages with invalid UTF-8 characters.
|
||||
* Additional nsd.conf files can be placed in /etc/nsd.conf.d.
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele
|
||||
|
||||
$ git clone https://github.com/mail-in-a-box/mailinabox
|
||||
$ cd mailinabox
|
||||
$ git checkout v0.54
|
||||
$ git checkout v55
|
||||
|
||||
Begin the installation.
|
||||
|
||||
|
||||
@@ -1740,7 +1740,7 @@ paths:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
/mfa/enable/totp:
|
||||
/mfa/totp/enable:
|
||||
post:
|
||||
tags:
|
||||
- MFA
|
||||
|
||||
@@ -21,7 +21,7 @@ import auth, utils
|
||||
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
||||
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
|
||||
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
|
||||
from mfa import get_public_mfa_state, provision_totp, provision_webauthn, validate_totp_secret, enable_mfa, disable_mfa
|
||||
from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa
|
||||
|
||||
env = utils.load_environment()
|
||||
|
||||
@@ -468,7 +468,7 @@ def ssl_provision_certs():
|
||||
def mfa_get_status():
|
||||
# Anyone accessing this route is an admin, and we permit them to
|
||||
# see the MFA status for any user if they submit a 'user' form
|
||||
# field. But we don't always include provisioning info since a user can
|
||||
# field. But we don't include provisioning info since a user can
|
||||
# only provision for themselves.
|
||||
email = request.form.get('user', request.user_email) # user field if given, otherwise the user making the request
|
||||
try:
|
||||
@@ -478,15 +478,14 @@ def mfa_get_status():
|
||||
if email == request.user_email:
|
||||
resp.update({
|
||||
"new_mfa": {
|
||||
"totp": provision_totp(email, env),
|
||||
"webauthn": provision_webauthn(email, env)
|
||||
"totp": provision_totp(email, env)
|
||||
}
|
||||
})
|
||||
except ValueError as e:
|
||||
return (str(e), 400)
|
||||
return json_response(resp)
|
||||
|
||||
@app.route('/mfa/enable/totp', methods=['POST'])
|
||||
@app.route('/mfa/totp/enable', methods=['POST'])
|
||||
@authorized_personnel_only
|
||||
def totp_post_enable():
|
||||
secret = request.form.get('secret')
|
||||
@@ -496,20 +495,7 @@ def totp_post_enable():
|
||||
return ("Bad Input", 400)
|
||||
try:
|
||||
validate_totp_secret(secret)
|
||||
enable_mfa(request.user_email, "totp", env, secret, token, label)
|
||||
except ValueError as e:
|
||||
return (str(e), 400)
|
||||
return "OK"
|
||||
|
||||
@app.route('/mfa/enable/webauthn', methods=['POST'])
|
||||
@authorized_personnel_only
|
||||
def webauthn_post_enable():
|
||||
attestationObject = request.form.get('attestationObject')
|
||||
clientDataJSON = request.form.get('clientDataJSON')
|
||||
if type(attestationObject) != str or type(clientDataJSON) != str:
|
||||
return ("Bad Input", 400)
|
||||
try:
|
||||
enable_mfa(request.user_email, "webauthn", env, attestationObject, clientDataJSON)
|
||||
enable_mfa(request.user_email, "totp", secret, token, label, env)
|
||||
except ValueError as e:
|
||||
return (str(e), 400)
|
||||
return "OK"
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import base64
|
||||
import hmac
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import pyotp
|
||||
import qrcode
|
||||
import pywarp
|
||||
import pywarp.backends
|
||||
|
||||
from mailconfig import open_database
|
||||
|
||||
@@ -32,44 +29,25 @@ def get_public_mfa_state(email, env):
|
||||
]
|
||||
|
||||
def get_hash_mfa_state(email, env):
|
||||
# Get the current MFA credential secrets from which we form a hash
|
||||
# so that we can reset user logins when any authentication information
|
||||
# changes.
|
||||
mfa_state = []
|
||||
for s in get_mfa_state(email, env):
|
||||
# Add TOTP id and secret to the state.
|
||||
# Skip WebAuthn state if it's just a challenge.
|
||||
if s["type"] == "webauthn":
|
||||
try:
|
||||
# Get the credential and only include it (not challenges) in the state.
|
||||
s["secret"] = json.loads(s["secret"])["cred_pub_key"]
|
||||
except:
|
||||
# Skip this one --- there is no cred_pub_key.
|
||||
continue
|
||||
mfa_state.append({ "id": s["id"], "type": s["type"], "secret": s["secret"] })
|
||||
return mfa_state
|
||||
mfa_state = get_mfa_state(email, env)
|
||||
return [
|
||||
{ "id": s["id"], "type": s["type"], "secret": s["secret"] }
|
||||
for s in mfa_state
|
||||
]
|
||||
|
||||
def enable_mfa(email, type, env, *args):
|
||||
def enable_mfa(email, type, secret, token, label, env):
|
||||
if type == "totp":
|
||||
secret, token, label = args
|
||||
validate_totp_secret(secret)
|
||||
# Sanity check with the provide current token.
|
||||
totp = pyotp.TOTP(secret)
|
||||
if not totp.verify(token, valid_window=1):
|
||||
raise ValueError("Invalid token.")
|
||||
conn, c = open_database(env, with_connection=True)
|
||||
c.execute('INSERT INTO mfa (user_id, type, secret, label) VALUES (?, ?, ?, ?)', (get_user_id(email, c), type, secret, label))
|
||||
conn.commit()
|
||||
elif type == "webauthn":
|
||||
attestationObject, clientDataJSON = args
|
||||
rp = pywarp.RelyingPartyManager(
|
||||
get_relying_party_name(env),
|
||||
rp_id=env["PRIMARY_HOSTNAME"], # must match hostname the control panel is served from
|
||||
credential_storage_backend=WebauthnStorageBackend(env))
|
||||
rp.register(attestation_object=base64.b64decode(attestationObject), client_data_json=base64.b64decode(clientDataJSON), email=email.encode("utf8")) # encoding of email is a little funky here, pywarp calls .decode() with no args?
|
||||
else:
|
||||
raise ValueError("Invalid MFA type.")
|
||||
|
||||
conn, c = open_database(env, with_connection=True)
|
||||
c.execute('INSERT INTO mfa (user_id, type, secret, label) VALUES (?, ?, ?, ?)', (get_user_id(email, c), type, secret, label))
|
||||
conn.commit()
|
||||
|
||||
def set_mru_token(email, mfa_id, token, env):
|
||||
conn, c = open_database(env, with_connection=True)
|
||||
@@ -93,9 +71,6 @@ def validate_totp_secret(secret):
|
||||
if len(secret) != 32:
|
||||
raise ValueError("Secret should be a 32 characters base32 string")
|
||||
|
||||
def get_relying_party_name(env):
|
||||
return env["PRIMARY_HOSTNAME"] + " Mail-in-a-Box Control Panel"
|
||||
|
||||
def provision_totp(email, env):
|
||||
# Make a new secret.
|
||||
secret = base64.b32encode(os.urandom(20)).decode('utf-8')
|
||||
@@ -104,7 +79,7 @@ def provision_totp(email, env):
|
||||
# Make a URI that we encode within a QR code.
|
||||
uri = pyotp.TOTP(secret).provisioning_uri(
|
||||
name=email,
|
||||
issuer_name=get_relying_party_name(env)
|
||||
issuer_name=env["PRIMARY_HOSTNAME"] + " Mail-in-a-Box Control Panel"
|
||||
)
|
||||
|
||||
# Generate a QR code as a base64-encode PNG image.
|
||||
@@ -119,55 +94,6 @@ def provision_totp(email, env):
|
||||
"qr_code_base64": png_b64
|
||||
}
|
||||
|
||||
class WebauthnStorageBackend(pywarp.backends.CredentialStorageBackend):
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
def get_record(self, email, conn=None, c=None):
|
||||
# Get an existing record and parse the 'secret' column as JSON.
|
||||
if conn is None: conn, c = open_database(self.env, with_connection=True)
|
||||
c.execute('SELECT secret FROM mfa WHERE user_id=? AND type="webauthn"', (get_user_id(email, c),))
|
||||
config = c.fetchone()
|
||||
if config:
|
||||
try:
|
||||
return json.loads(config[0])
|
||||
except:
|
||||
pass
|
||||
return { }
|
||||
def update_record(self, email, fields):
|
||||
# Update the webauthn record in the database for this user by
|
||||
# merging the fields with the existing fields in the database.
|
||||
conn, c = open_database(self.env, with_connection=True)
|
||||
config = self.get_record(email, conn=conn, c=c)
|
||||
if config:
|
||||
# Merge and update.
|
||||
config.update(fields)
|
||||
config = json.dumps(config)
|
||||
c.execute('UPDATE mfa SET secret=? WHERE user_id=? AND type="webauthn"', (config, get_user_id(email, c),))
|
||||
conn.commit()
|
||||
return
|
||||
|
||||
# Either there's no existing webauthn record or it's corrupted. Delete any existing record.
|
||||
# Then add a new record.
|
||||
c.execute('DELETE FROM mfa WHERE user_id=? AND type="webauthn"', (get_user_id(email, c),))
|
||||
c.execute('INSERT INTO mfa (user_id, type, secret, label) VALUES (?, ?, ?, ?)', (
|
||||
get_user_id(email, c), "webauthn",
|
||||
json.dumps(fields),
|
||||
"WebAuthn"))
|
||||
conn.commit()
|
||||
def save_challenge_for_user(self, email, challenge, type):
|
||||
self.update_record(email, { type + "challenge": base64.b64encode(challenge).decode("ascii") })
|
||||
def get_challenge_for_user(self, email, type):
|
||||
challenge = self.get_record(email).get(type + "challenge")
|
||||
if challenge: challenge = base64.b64decode(challenge.encode("ascii"))
|
||||
return challenge
|
||||
|
||||
def provision_webauthn(email, env):
|
||||
rp = pywarp.RelyingPartyManager(
|
||||
get_relying_party_name(env),
|
||||
rp_id=env["PRIMARY_HOSTNAME"], # must match hostname the control panel is served from
|
||||
credential_storage_backend=WebauthnStorageBackend(env))
|
||||
return rp.get_registration_options(email=email)
|
||||
|
||||
def validate_auth_mfa(email, request, env):
|
||||
# Validates that a login request satisfies any MFA modes
|
||||
# that have been enabled for the user's account. Returns
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<p class="alert" role="alert">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
You may encounter zone file errors when attempting to create a TXT record with a long string.
|
||||
<a href="http://tools.ietf.org/html/rfc4408#section-3.1.3">RFC 4408</a> states a TXT record is allowed to contain multiple strings, and this technique can be used to construct records that would exceed the 255-byte maximum length.
|
||||
<a href="https://tools.ietf.org/html/rfc4408#section-3.1.3">RFC 4408</a> states a TXT record is allowed to contain multiple strings, and this technique can be used to construct records that would exceed the 255-byte maximum length.
|
||||
You may need to adopt this technique when adding DomainKeys. Use a tool like <code>named-checkzone</code> to validate your zone file.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
<style>
|
||||
.twofactor #totp-setup,
|
||||
.twofactor #disable-2fa,
|
||||
.twofactor #output-2fa {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.twofactor.loaded .loading-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.twofactor.disabled #disable-2fa,
|
||||
.twofactor.enabled #totp-setup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.twofactor.disabled #totp-setup,
|
||||
.twofactor.enabled #disable-2fa {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.twofactor #totp-setup-qr img {
|
||||
display: block;
|
||||
width: 256px;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.twofactor #output-2fa.visible {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h2>Two-Factor Authentication</h2>
|
||||
@@ -27,11 +51,10 @@ and ensure every administrator account for this control panel does the same.</st
|
||||
</div>
|
||||
|
||||
<div class="twofactor">
|
||||
<div id="mfa-devices">
|
||||
</div>
|
||||
<div class="loading-indicator">Loading...</div>
|
||||
|
||||
<form id="totp-setup" style="display: none">
|
||||
<h3>Add a TOTP Device</h3>
|
||||
<form id="totp-setup">
|
||||
<h3>Setup Instructions</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<p>1. Install <a href="https://freeotp.github.io/">FreeOTP</a> or <a href="https://www.pcworld.com/article/3225913/what-is-two-factor-authentication-and-which-2fa-apps-are-best.html">any
|
||||
@@ -62,32 +85,24 @@ and ensure every administrator account for this control panel does the same.</st
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form id="webauthn-setup" style="display: none">
|
||||
<h3>Add a WebAuthn Device</h3>
|
||||
<p>If you have a WebAuthn device such as a YubiKey, plug it in and click Add WebAuthn Device.</p>
|
||||
<button type="submit" class="btn" onclick="return do_enable_webauthn()">Add WebAuthn Device</button>
|
||||
<form id="disable-2fa">
|
||||
<div class="form-group">
|
||||
<p>Two-factor authentication is active for your account<span id="mfa-device-label"></span>.</p>
|
||||
<p>You will have to log into the admin panel again after disabling two-factor authentication.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-danger">Disable Two-Factor Authentication</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="webauthn-setup" style="display: none">
|
||||
</div>
|
||||
|
||||
<div id="output-2fa" class="panel panel-danger hidden">
|
||||
<div id="output-2fa" class="panel panel-danger">
|
||||
<div class="panel-body"></div>
|
||||
</div>
|
||||
|
||||
<div id="mfa-device-templates" style="display: none">
|
||||
<form class="totp" style="margin: 1em 0; border: 1px solid #AAA; padding: 10px;">
|
||||
<p>Two-factor authentication is active for your account<span class="mfa-device-label"></span>.</p>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-danger">Disable TOTP Device</button>
|
||||
</div>
|
||||
<p style="margin-bottom: 0">You will have to log into the admin panel again after disabling two-factor authentication.</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var el = {
|
||||
disableForm: document.getElementById('disable-2fa'),
|
||||
output: document.getElementById('output-2fa'),
|
||||
totpSetupForm: document.getElementById('totp-setup'),
|
||||
totpSetupToken: document.getElementById('totp-setup-token'),
|
||||
@@ -95,7 +110,6 @@ and ensure every administrator account for this control panel does the same.</st
|
||||
totpSetupLabel: document.getElementById('totp-setup-label'),
|
||||
totpQr: document.getElementById('totp-setup-qr'),
|
||||
totpSetupSubmit: document.querySelector('#totp-setup-submit'),
|
||||
webauthnSetupForm: document.getElementById('webauthn-setup'),
|
||||
wrapper: document.querySelector('.twofactor')
|
||||
}
|
||||
|
||||
@@ -116,8 +130,6 @@ and ensure every administrator account for this control panel does the same.</st
|
||||
}
|
||||
|
||||
function render_totp_setup(provisioned_totp) {
|
||||
$(el.totpSetupForm).show();
|
||||
|
||||
var img = document.createElement('img');
|
||||
img.src = "data:image/png;base64," + provisioned_totp.qr_code_base64;
|
||||
|
||||
@@ -135,50 +147,38 @@ and ensure every administrator account for this control panel does the same.</st
|
||||
el.wrapper.classList.add('disabled');
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(a) { return btoa(String.fromCharCode(...new Uint8Array(a))); }
|
||||
function base64ToArrayBuffer(b) { return Uint8Array.from(atob(b), c => c.charCodeAt(0)); }
|
||||
|
||||
function render_webauthn_setup(provisioning) {
|
||||
$(el.webauthnSetupForm).show();
|
||||
provisioning.challenge = base64ToArrayBuffer(provisioning.challenge);
|
||||
provisioning.user.id = new TextEncoder().encode(provisioning.user.name);
|
||||
window.mailinabix_mfa_webauthn_provision = provisioning;
|
||||
}
|
||||
|
||||
function render_disable(mfa) {
|
||||
var panel = $('#mfa-device-templates .' + mfa.type).clone();
|
||||
$('#mfa-devices').append(panel);
|
||||
panel.attr('data-mfa-id', mfa.id);
|
||||
panel.on('submit', do_disable);
|
||||
el.disableForm.addEventListener('submit', do_disable);
|
||||
el.wrapper.classList.add('enabled');
|
||||
if (mfa.label)
|
||||
panel.find(".mfa-device-label").text(" on device '" + mfa.label + "'");
|
||||
$("#mfa-device-label").text(" on device '" + mfa.label + "'");
|
||||
}
|
||||
|
||||
function hide_error() {
|
||||
el.output.querySelector('.panel-body').innerHTML = '';
|
||||
el.output.classList.add('hidden');
|
||||
el.output.classList.remove('visible');
|
||||
}
|
||||
|
||||
function render_error(msg) {
|
||||
el.output.querySelector('.panel-body').innerHTML = msg;
|
||||
el.output.classList.remove('hidden');
|
||||
el.output.classList.add('visible');
|
||||
}
|
||||
|
||||
function reset_view() {
|
||||
el.wrapper.classList.remove('loaded', 'disabled', 'enabled');
|
||||
$('#mfa-devices > *').remove();
|
||||
|
||||
el.disableForm.removeEventListener('submit', do_disable);
|
||||
|
||||
hide_error();
|
||||
|
||||
$(el.totpSetupForm).hide();
|
||||
el.totpSetupForm.reset();
|
||||
el.totpSetupForm.removeEventListener('submit', do_enable_totp);
|
||||
|
||||
el.totpSetupSecret.setAttribute('value', '');
|
||||
el.totpSetupToken.removeEventListener('input', update_setup_disabled);
|
||||
|
||||
el.totpSetupSubmit.setAttribute('disabled', '');
|
||||
el.totpQr.innerHTML = '';
|
||||
|
||||
$(el.webauthnSetupForm).hide();
|
||||
}
|
||||
|
||||
function show_mfa() {
|
||||
@@ -191,16 +191,16 @@ and ensure every administrator account for this control panel does the same.</st
|
||||
function(res) {
|
||||
el.wrapper.classList.add('loaded');
|
||||
|
||||
var has_mfa = false;
|
||||
res.enabled_mfa.forEach(function(mfa) {
|
||||
if (mfa.type == "totp") {
|
||||
render_disable(mfa);
|
||||
has_mfa = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (res.new_mfa.totp)
|
||||
render_totp_setup(res.new_mfa.totp);
|
||||
if (res.new_mfa.webauthn && 'credentials' in navigator)
|
||||
render_webauthn_setup(res.new_mfa.webauthn);
|
||||
if (!has_mfa)
|
||||
render_totp_setup(res.new_mfa.totp);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -212,7 +212,7 @@ and ensure every administrator account for this control panel does the same.</st
|
||||
api(
|
||||
'/mfa/disable',
|
||||
'POST',
|
||||
{ id: $(this).attr('data-mfa-id') },
|
||||
{ type: 'totp' },
|
||||
function() {
|
||||
do_logout();
|
||||
}
|
||||
@@ -226,7 +226,7 @@ and ensure every administrator account for this control panel does the same.</st
|
||||
hide_error();
|
||||
|
||||
api(
|
||||
'/mfa/enable/totp',
|
||||
'/mfa/totp/enable',
|
||||
'POST',
|
||||
{
|
||||
token: $(el.totpSetupToken).val(),
|
||||
@@ -239,22 +239,4 @@ and ensure every administrator account for this control panel does the same.</st
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function do_enable_webauthn() {
|
||||
navigator.credentials.create({ publicKey: window.mailinabix_mfa_webauthn_provision })
|
||||
.then(function(creds) {
|
||||
api(
|
||||
'/mfa/enable/webauthn',
|
||||
'POST',
|
||||
{
|
||||
attestationObject: arrayBufferToBase64(creds.response['attestationObject']),
|
||||
clientDataJSON: arrayBufferToBase64(creds.response['clientDataJSON'])
|
||||
},
|
||||
function(res) { do_logout(); },
|
||||
function(res) { render_error(res); }
|
||||
);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
|
||||
<table class="table">
|
||||
<thead><tr><th>For...</th> <th>Use...</th></tr></thead>
|
||||
<tr><td>Contacts and Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVdroid</a> ($3.69; free <a href="https://f-droid.org/packages/at.bitfire.davdroid/">here</a>)</td></tr>
|
||||
<tr><td>Only Contacts</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.carddav.sync">CardDAV-Sync free beta</a> (free)</td></tr>
|
||||
<tr><td>Only Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.caldav.lib">CalDAV-Sync</a> ($2.89)</td></tr>
|
||||
<tr><td>Contacts and Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVx⁵</a> ($5.99; free <a href="https://f-droid.org/packages/at.bitfire.davdroid/">here</a>)</td></tr>
|
||||
<tr><td>Only Contacts</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.carddav.sync">CardDAV-Sync free</a> (free)</td></tr>
|
||||
<tr><td>Only Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.caldav.lib">CalDAV-Sync</a> ($2.99)</td></tr>
|
||||
</table>
|
||||
|
||||
<p>Use the following settings:</p>
|
||||
|
||||
@@ -20,7 +20,7 @@ if [ -z "$TAG" ]; then
|
||||
# want to display in status checks.
|
||||
if [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' )" == "Ubuntu 18.04 LTS" ]; then
|
||||
# This machine is running Ubuntu 18.04.
|
||||
TAG=v0.54
|
||||
TAG=v55
|
||||
|
||||
elif [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' )" == "Ubuntu 14.04 LTS" ]; then
|
||||
# This machine is running Ubuntu 14.04.
|
||||
|
||||
@@ -50,7 +50,7 @@ hide_output $venv/bin/pip install --upgrade pip
|
||||
hide_output $venv/bin/pip install --upgrade \
|
||||
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
|
||||
flask dnspython python-dateutil expiringdict \
|
||||
qrcode[pil] pyotp pywarp \
|
||||
qrcode[pil] pyotp \
|
||||
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver b2sdk
|
||||
|
||||
# CONFIGURATION
|
||||
|
||||
@@ -29,8 +29,8 @@ apt_install \
|
||||
# Combine the Roundcube version number with the commit hash of plugins to track
|
||||
# whether we have the latest version of everything.
|
||||
|
||||
VERSION=1.5-rc
|
||||
HASH=a7cb2a39702536d769c7ff93f716e27f0b93f9d9
|
||||
VERSION=1.5.0
|
||||
HASH=2a9d11d9c10c8e8756120606c47eef702f00fe6d
|
||||
PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435 # version 5.2.0
|
||||
HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+
|
||||
CARDDAV_VERSION=3.0.3
|
||||
|
||||
Reference in New Issue
Block a user