Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/app/css/navbar-top-fixed.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ body {
flex-direction: column;
}

.navbar-dark .nav-link,
.navbar[data-bs-theme=dark] .nav-link {
color: var(--bs-gray-200);
}

#tabs-content > * {
height: 100%;
width: 100%;
Expand Down
2 changes: 0 additions & 2 deletions src/common/libSieve/toolkit/utils/SieveI18n.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ const DEFAULT_PATH = "./i18n/";
const LANGUAGES = new Set();
LANGUAGES.add("en-US");
LANGUAGES.add("de-DE");
LANGUAGES.add("hu-HU");
LANGUAGES.add("cz-CZ");

// Maps a language to a supported language.
const LANGUAGE_MAPPING = new Map();
LANGUAGE_MAPPING.set("en", "en-US");
LANGUAGE_MAPPING.set("de", "de-DE");
LANGUAGE_MAPPING.set("hu", "hu-HU");
LANGUAGE_MAPPING.set("cz", "cz-CZ");

let instance = null;
Expand Down
2 changes: 0 additions & 2 deletions src/common/managesieve.ui/utils/SieveI18n.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,12 @@ const DEFAULT_PATH = "./i18n/";
const LANGUAGES = new Set();
LANGUAGES.add("en-US");
LANGUAGES.add("de-DE");
LANGUAGES.add("hu-HU");
LANGUAGES.add("cz-CZ");

// Maps a language to a supported language.
const LANGUAGE_MAPPING = new Map();
LANGUAGE_MAPPING.set("en", "en-US");
LANGUAGE_MAPPING.set("de", "de-DE");
LANGUAGE_MAPPING.set("hu", "hu-HU");
LANGUAGE_MAPPING.set("cz", "cz-CZ");

let instance = null;
Expand Down
17 changes: 10 additions & 7 deletions src/web/config.template.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
# The webserver settings
ServerPort = 8765

# Specify on which address the server should be bound and starts listening.
# Specify on which address the server should be bound and start listening.
#
# If you pass an empty string it will autmatically bind to
# If you pass an empty string it will automatically bind to
# all available ip addresses configured
ServerAddress = 127.0.0.1

# Whether to use SSL. When behind a reverse proxy, it may be unnecessary. In that
# case ServerCertFile and ServerKeyFile settings are ignored.
UseSSL = True
# The location of the key and certificate file.
ServerCertFile = d:\something\secure\sieve.cert
ServerKeyFile = d:\something\secure\sieve.key

# The sieve servers settings
# Sieve server settings
SieveHost = imap.example.com
SievePort = 4190

# Two authentication types are upported, a client side and a server side.
# Two authentication types are supported, a client side and a server side.
#
# Server side authentication means a proxy authorization will be performed on
# the server side. This means no password prompt will be shown on the client.
Expand All @@ -26,12 +29,12 @@ SievePort = 4190
# In this case the users does not need to enter any password.
#
# Client side authentication means the authentication is performed in the users
# browser, thus the client controlls the whole authentication process and needs
# browser, thus the client controls the whole authentication process and needs
# to provide his password though the UI. Keep in mind you still need to provide
# the username from the server side, either through injecting the username though
# the reverse procy or hardcoded in this config.
# the reverse proxy or hardcoded in this config.
#
# It default to client side authentication.
# It defaults to client side authentication.
AuthType = client


Expand Down
12 changes: 4 additions & 8 deletions src/web/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,15 @@

parser = ArgumentParser(description='Starts the websocket to sieve proxy.')
parser.add_argument("--config", help="The configuration file if omitted it will fallback to ./config.ini")
parser.add_argument('--verbose', '-v', action='count', default=1)
parser.add_argument('--verbose', '-v', action='count')

args = parser.parse_args()

args.verbose = 40 - (10*args.verbose) if args.verbose > 0 else 0
args.verbose = max(10, 40 - (10*args.verbose)) if args.verbose > 0 else 0

logging.basicConfig(level=args.verbose, format='%(asctime)s %(levelname)s [%(funcName)s] %(filename)s : %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')

logging.debug('im a DEBUG message')
logging.info('im a INFO message')
logging.warning('im a WARNING message')
logging.critical('im a CRITICAL message')
logging.error('im a ERROR message')
logging.info(f"Application started. Log level: {logging.getLevelName(args.verbose)}")

if args.config is None:
args.config = "config.ini"
Expand All @@ -45,6 +40,7 @@
webServer = WebServer(
address = config.get_address(),
port = config.get_port(),
use_ssl = config.get_use_ssl(),
keyfile = config.get_keyfile(),
certfile = config.get_certfile())

Expand Down
48 changes: 39 additions & 9 deletions src/web/script/config/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import configparser
import pathlib
import hashlib
import logging
import pathlib

class NoSuchPropertyException(Exception):
pass
Expand All @@ -17,7 +18,7 @@ def _get_property(self, name):
if self._has_property(name):
return self._config[self._section][name]

raise NoSuchPropertyException("Unknown Property "+name)
raise NoSuchPropertyException(f'Unknown Property "{name}"')

def get_id(self):
"""
Expand Down Expand Up @@ -56,14 +57,18 @@ class SieveClientAccount(SieveAccount):
def get_auth_username(self, request):

if self._has_property("AuthUser"):
return self._config[self._section]["AuthUser"]
user = self._config[self._section]["AuthUser"]
logging.debug(f"Sieve user with client auth type and pre-configured AuthUser: {user}")
return user

header = self._get_property("AuthUserHeader")

if request.get_header(header) is None:
raise NoSuchPropertyException(f"No header {header} in request.")

return request.get_header(header)
user = request.get_header(header)
logging.debug(f"Sieve user with client auth type via {header} header: {user}")
return user

def can_authorize(self):
return self._config[self._section].getboolean("AuthClientAuthorization", fallback=False)
Expand All @@ -86,7 +91,9 @@ def get_sieve_user(self, request):
if request.get_header(header) is None:
raise NoSuchPropertyException(f"No header {header} in request.")

return request.get_header(header)
user = request.get_header(header)
logging.debug(f"Sieve user with token auth type via {header} header: {user}")
return user

def get_sieve_password(self, request):

Expand Down Expand Up @@ -115,11 +122,19 @@ def get_auth_username(self, request):
header = self._get_property("AuthUserHeader")

if request.get_header(header) is not None:
return request.get_header(header)
user = request.get_header(header)
logging.debug(f"Sieve user with authorization auth type via {header} header: {user}")
return user
else:
logging.warning(f"{header} header didn't arrive to identify Sieve user with authorization auth type. "
"Falling back to AuthUser config option.")

## FIXME only temporarily disabled
#raise NoSuchPropertyException("Invalid username")
return self._config[self._section]["AuthUser"]

user = self._config[self._section]["AuthUser"]
logging.debug(f"Sieve user with authorization auth type and pre-configured AuthUser: {user}")
return user


class Config:
Expand Down Expand Up @@ -151,17 +166,32 @@ def get_address(self):
"""
return self._config["DEFAULT"]["ServerAddress"]

def get_use_ssl(self):
"""
Whether to use SSL/HTTPS
"""
if "UseSSL" in self._config["DEFAULT"]:
return self._config["DEFAULT"].getboolean("UseSSL")
else:
return True

def get_keyfile(self):
"""
The keyfile used for securing the server.
"""
return self._config["DEFAULT"]["ServerKeyFile"]
if self.get_use_ssl() and "ServerKeyFile" in self._config["DEFAULT"]:
return self._config["DEFAULT"]["ServerKeyFile"]
else:
return None

def get_certfile(self):
"""
The certificate file used for securing the server.
"""
return self._config["DEFAULT"]["ServerCertFile"]
if self.get_use_ssl() and "ServerCertFile" in self._config["DEFAULT"]:
return self._config["DEFAULT"]["ServerCertFile"]
else:
return None

def get_auth_type(self, section : str):

Expand Down
6 changes: 4 additions & 2 deletions src/web/script/handler/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def handle_request(self, context, request) -> None:
host = account.get_sieve_host()
port = int(account.get_sieve_port())

logging.debug(f'Communicating with "{host}:{port}" with auth username "{account.get_auth_username(request)}"')

# Websocket is read
with WebSocket(request, context) as websocket:
with SieveSocket(host, port) as sievesocket:
Expand All @@ -42,7 +44,7 @@ def handle_request(self, context, request) -> None:
account.get_auth_username(request))

# Publish capabilities to client...
websocket.send(
sievesocket.capabilities)
logging.debug(f'Publishing capabilities to client: "{sievesocket.capabilities.decode()}"')
websocket.send(sievesocket.capabilities)

MessagePump().run(websocket, sievesocket)
16 changes: 13 additions & 3 deletions src/web/script/http.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import select
import time

Expand Down Expand Up @@ -68,9 +69,18 @@ def recv(self, context, blocking : bool = True) -> None:

for header in headers:
header = header.split(":", 1)
self.__headers[header[0]] = header[1].lstrip()

self.__payload = data[1]
value = header[1].lstrip()

if value:
self.__headers[header[0]] = value
else:
logging.warning(f"Received header {header[0]} is empty.")

if len(data) > 1:
self.__payload = data[1]
else:
self.__payload = ""
logging.warning(f"Received request with empty body")


class HttpResponse:
Expand Down
2 changes: 1 addition & 1 deletion src/web/script/sieve/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def decode(self, data) -> 'Response':
self._capabilities[key.upper()] = value

if b'"IMPLEMENTATION"' not in self._capabilities:
raise Exception("Implementation expected")
raise Exception("IMPLEMENTATION expected")

super().decode(parser.get_data())

Expand Down
37 changes: 24 additions & 13 deletions src/web/script/sieve/sievesocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def __init__(self, hostname : str, port : int):

self.__hostname = hostname
self.__port = port
self.__timeout = 3.0

def __enter__(self):
self.connect()
Expand All @@ -30,17 +31,11 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> None:

def connect(self):
self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.__socket.settimeout(self.__timeout)
self.__socket.connect((self.__hostname, self.__port))

capabilities = Capabilities()
capabilities.decode(self.recv())

if b'"SASL"' not in capabilities.get_capabilities():
raise Exception("Sasl Plain not supported")

if b'PLAIN' not in capabilities.get_capabilities()[b'"SASL"'][1:-1].split(b" "):
raise Exception("Sasl Plain not supported")

self.__capabilities = capabilities

def __del__(self) -> None:
Expand All @@ -65,6 +60,7 @@ def upgrade(self) -> None:
ssl_context.load_default_certs()

self.__socket = ssl_context.wrap_socket(self.__old_socket)
self.__socket.settimeout(self.__timeout)

def wait(self):
while True:
Expand All @@ -81,39 +77,54 @@ def recv(self):
chunk = self.__socket.recv(1024*1024)

if chunk == "":
raise Exception("Connetion Terminated...")
raise Exception("Connection terminated")

return chunk

def send(self, data: str) -> None:
def send(self, data: bytes) -> None:
self.__socket.send(data)

def start_tls(self) -> None:
logging.debug("Securing connection with STARTTLS")

if b'"STARTTLS"' not in self.__capabilities.get_capabilities():
raise Exception("Starttls not supported")
raise Exception("STARTTLS is not supported")

self.send(b"STARTTLS\r\n")

if Response().decode(self.recv()).status != "OK" :
raise Exception("Starting tls failed")
raise Exception("Starting TLS failed")

self.upgrade()

#update the capabilities
# update the capabilities
self.__capabilities.decode(self.recv())

# Check SASL capabilities here.
# Some implementations don't allow SASL auth without using a secure connection (i.e. before STARTTLS).
# In those cases, without TLS, the SASL support list is empty.
if b'"SASL"' not in self.__capabilities.get_capabilities():
raise Exception("SASL is not supported")

if b'PLAIN' not in self.__capabilities.get_capabilities()[b'"SASL"'][1:-1].split(b" "):
raise Exception("SASL PLAIN is not supported")

def authenticate(self, authentication: str, password: str, authorization:str ) -> None:

self.__capabilities.disable_authentication()

logging.debug(f'Going to authenticate with authorization "{authorization}" and authentication "{authentication}"')

self.send(
b'AUTHENTICATE "PLAIN" "'+b64encode(
authorization.encode()+b"\0"
+ authentication.encode()+b"\0"
+ password.encode())+b'"\r\n')

if Response().decode(self.recv()).status != "OK" :
response = self.recv()
logging.debug(f'Auth response: "{response.decode().strip()}"')

if Response().decode(response).status != "OK" :
raise Exception("Authentication failed")

@property
Expand Down
Loading
Loading