Compare commits

..

No commits in common. "master" and "php-version-frozen" have entirely different histories.

36 changed files with 729 additions and 814 deletions

1
.gitignore vendored
View file

@ -1,2 +1 @@
config config
src/config

View file

@ -1,19 +1,15 @@
FROM golang:1-alpine3.14 AS builder FROM php:7.4-apache
COPY src/ /mailautoconf COPY src/ /var/www/html/
WORKDIR /mailautoconf
RUN go build -o /mailautoconf/mailautoconf
FROM alpine:3.14
RUN apk add --no-cache bash
COPY --from=builder /mailautoconf/mailautoconf /mailautoconf/mailautoconf
COPY --from=builder /mailautoconf/default-config /mailautoconf/default-config
COPY --from=builder /mailautoconf/templates /mailautoconf/templates
COPY ./entrypoint.sh / COPY ./entrypoint.sh /
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
EXPOSE 8010 # Use the default production configuration
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
RUN a2enmod rewrite
EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]

View file

@ -1,12 +1,5 @@
# MailAutoConf - a simple, configurable autodiscover/autoconfig service for distributed and self-hosted services. # MailAutoConf - a simple, configurable autodiscover/autoconfig service for distributed and self-hosted services.
## New GoLang version - please make sure you update your ini files to yaml!
Github : https://github.com/pswilde/mailautoconf
<a href="https://www.buymeacoffee.com/pswilde" target="_blank">
<img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" height="60" width="217" alt="Buy Me A Coffee" style="height: 30px !important;width: 106px !important;" >
</a>
## What is MailAutoConf? ## What is MailAutoConf?
MailAutoConf is autodiscover/autoconfig web server for self-hosted mail services MailAutoConf is autodiscover/autoconfig web server for self-hosted mail services
which do not have their own autodiscover service. which do not have their own autodiscover service.
@ -20,14 +13,14 @@ https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat, should
be able to auto-configure using this service. be able to auto-configure using this service.
## Installation ## Installation
MailAutoConf runs its own webserver on port 8010. The container file includes set up for an apache webserver to run the application.
You will need to supply a volume for the configuration file and port forwarding. You will need to supply a volume for the configuration file and port forwarding.
``` ```
podman run -dt \ podman run -dt \
--name mailautoconf \ --name mailautoconf \
-v ./config:/mailautoconf/config \ -v ./config:/var/www/html/config \
-p 8010:8010 \ -p 8010:80 \
pswilde/mailautoconf pswilde/automailconf
``` ```
You will need a reverse proxy server to publish to the outside world and handle SSL encryption. You will need a reverse proxy server to publish to the outside world and handle SSL encryption.
For example, in nginx: For example, in nginx:
@ -47,7 +40,7 @@ server {
} }
} }
``` ```
First run will create sample.yaml files in the config directory. Copy these to config.yaml and services.yaml and configure them to your needs. First run will create sample.ini files in the config directory. Copy these to config.ini and services.ini and configure them to your needs.
MailAutoConf will handle all the URLs it's able to deal with, i.e. /mail/config-v1.1.xml, /Autodiscover/Autodiscover.xml automatically. MailAutoConf will handle all the URLs it's able to deal with, i.e. /mail/config-v1.1.xml, /Autodiscover/Autodiscover.xml automatically.
@ -64,7 +57,7 @@ SRV _autodiscover._tcp.your.domain 3600 10 10 443 autoconfig.your.domain
``` ```
## Compatibility ## Compatibility
MailAutoConf has been tested and confirmed working (for IMAP and SMTP) with the following software packages MailAutoConf has been tested and confirmed working with the following software packages
- [x] Thunderbird (v78 and probably earlier versions too) - [x] Thunderbird (v78 and probably earlier versions too)
- [x] Evolution Mail (v3.40.3 and probably earlier versions too) - [x] Evolution Mail (v3.40.3 and probably earlier versions too)
- [x] Nextcloud Mail app - [x] Nextcloud Mail app
@ -90,11 +83,8 @@ Calendar and AddressBook is in the autoconfig XML documentation, but currently n
## When will it be ready for production? ## When will it be ready for production?
It works for non-Microsoft email clients now (see Compatibility above). Well, not yet. Though it does sort of work already.
Outlook's autodiscover is a troublesome little blighter, MailAutoConf does generate a valid Autodiscover.xml, but modern Outlook clients use an Autodiscover.json file now which isn't documented anywhere. I'm working on this and hope to get Outlook Compatibility as soon as possible. I'm working on this ultimately for my own use for my own small business. I'm hoping once it's good enough I could deploy the set up to my businesses customers and ultimately get them away from a Microsoft Exchange based environment. There's a long way to go for that right now though.
Then it's down to Autoconfiguration of Calendars and AddressBooks... but that's down to the email client developers really...
If you feel you may be able to help, or ideas on features and their implementation, notice any bugs, or just want to say hi. Please do so and submit a pull request if required. If you feel you may be able to help, or ideas on features and their implementation, notice any bugs, or just want to say hi. Please do so and submit a pull request if required.

View file

@ -3,7 +3,7 @@ services:
mailautoconf: mailautoconf:
container_name: mailautoconf container_name: mailautoconf
ports: ports:
- '8010:8010' - '8010:80'
volumes: volumes:
- './config:/mailautoconf/config' - './config:/var/www/html/config'
image: pswilde/mailautoconf image: pswilde/mailautoconf

View file

@ -1,31 +1,29 @@
#!/usr/bin/env bash #!/usr/bin/env bash
echo Removing old sample files… echo Removing old sample files…
rm /mailautoconf/config/*.sample.yaml rm /var/www/html/config/*.sample.ini
function write_file() { function write_file() {
while read line; while read line;
do do
first_char=${line:0:1} first_char=${line:0:1}
first_vers=${line:0:7}
if [[ $first_vers == "Version" ]]; then if [[ $first_char != ";" ]]; then
line="# This is the sample config file for MailAutoConf "$line line="; "$line
elif [[ $first_char != "#" ]]; then
line="#"$line
fi fi
echo $line >> $2 echo $line >> $2
done < $1 done < $1
} }
echo Setting up new sample config files… echo Setting up new sample config files…
def_conf="/mailautoconf/default-config/config.default.yaml" def_conf="/var/www/html/default-config/config.default.ini"
new_conf="/mailautoconf/config/config.sample.yaml" new_conf="/var/www/html/config/config.sample.ini"
write_file $def_conf $new_conf write_file $def_conf $new_conf
def_serv="/mailautoconf/default-config/services.default.yaml" def_serv="/var/www/html/default-config/services.default.ini"
new_serv="/mailautoconf/config/services.sample.yaml" new_serv="/var/www/html/config/services.sample.ini"
write_file $def_serv $new_serv write_file $def_serv $new_serv
echo New sample files copied
cd /mailautoconf
exec /mailautoconf/mailautoconf echo Running HTTPD…
exec apache2-foreground

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
podman run --name mailautoconf \ podman run --name mailautoconf \
--rm \ --rm \
-p "8010:8010" \ -p "8010:80" \
-v ./config:/mailautoconf/config \ -v ./config:/var/www/html/config \
pswilde/mailautoconf pswilde/mailautoconf

4
src/.htaccess Normal file
View file

@ -0,0 +1,4 @@
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.php?path=$1 [NC,L,QSA]

3
src/core/db/db.php Normal file
View file

@ -0,0 +1,3 @@
<?php
echo "No Db...yet.";
?>

57
src/core/init.php Normal file
View file

@ -0,0 +1,57 @@
<?php
// this class starts the whole thing going.
class Init {
public function start (){
// define a constant for checking later on
define("LETSGO",true);
// require the Load class which also imports the Core class
require("init/loader.php");
// Not sure if we're using a database yet, but leaving this here.
//require("db/db.php");
// Get the config
self::get_config();
// A Session may not be necessary as this is fundamentally an API
//Core::StartSession();
// Continue and load the requested page
$loader = new Loader();
$loader->request_page();
}
private function get_config(){
// parse the default config file for default values
$default_config = parse_ini_file(Core::root_dir()."/default-config/config.default.ini", true);
// define the custom config file location
$config_file = Core::root_dir()."/config/config.ini";
$config = array();
if (file_exists($config_file)) {
// if a custom config file exists then parse that file too
$config = parse_ini_file($config_file, true);
}
// merge the default config with the custom config
Core::$Config = array_merge($default_config,$config);
Core::$Config["PrimaryDomain"] = Core::$Config["Domain"][0];
// parse the default services file for default values
$default_services = parse_ini_file(Core::root_dir()."/default-config/services.default.ini", true);
// define the custom services file location
$services_file = Core::root_dir()."/config/services.ini";
$services = array();
if (file_exists($services_file)) {
// if a custom config file exists then parse that file too
$services = parse_ini_file($services_file, true);
}
// merge the default config with the custom config
// using replace recursive as the config file contains arrays itself
Core::$Config["Services"] = array_replace_recursive($default_services,$services);
// get the current git commit, if it exists. For testing
Core::$Config["CommitID"] = Core::get_current_git_commit();
}
}

77
src/core/init/core.php Normal file
View file

@ -0,0 +1,77 @@
<?php class Core {
public static $Config;
public const VERSION = "0.0.7";
public static $CurrentPage;
public static function root_dir(){
return $_SERVER['DOCUMENT_ROOT'];
}
public static function get_post_data(){
// Gets the POST request into an array
$data = [];
foreach($_POST as $key=>$value){
$data[$key] = $value;
}
return $data;
}
public static function get_get_data(){
// Gets the GET request into an array
$data = [];
foreach($_GET as $key=>$value){
$data[$key] = $value;
}
return $data;
}
public static function random_string($length = 10) {
// Generates a random string - unused currently
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
public static function start_session(){
// Starts a session, unused currently
session_start();
}
public static function end_session($redirect = true){
// Unset all of the session variables
$_SESSION = array();
setcookie("user_session", "", time() - 3600);
// Destroy the session.
session_destroy();
if($redirect){
header("location: /?e=LogoutSuccess");
}
}
public static function get_current_git_commit( $branch='master' ) {
// Gets the current git commit ID - for testing
$gitref = sprintf( self::root_dir().'/../.git/refs/heads/%s', $branch );
if ( file_exists($gitref) && $hash = file_get_contents( $gitref ) ) {
return trim($hash);
} else {
return false;
}
}
public static function full_url(){
// Gets the full requested URL
if(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
$addr = "https";
} else {
$addr = "http";
}
// Here append the common URL characters.
$addr .= "://";
// Append the host(domain name, ip) to the URL.
$addr .= $_SERVER['HTTP_HOST'];
// Append the requested resource location to the URL
$addr .= $_SERVER['REQUEST_URI'];
// Return the address
return $addr;
}
}

34
src/core/init/errors.php Normal file
View file

@ -0,0 +1,34 @@
<?php
class Errors {
public function throw_error($err){
header('Content-Type: application/json'); // <-- header declaration
$error = false;
// Some Saved error messages - more to be added
switch ($err) {
case "NotFound":
$error = new ErrorMessage("Error","404","Not Found");
break;
case "Unauthorized":
$error = new ErrorMessage("Error","402","Unauthorized");
break;
default:
$error = new ErrorMessage("Error","500","Internal Error");
break;
}
// Output error in JSON format
echo json_encode($error, true); // <--- encode
}
}
class ErrorMessage {
public $error;
public $code;
public $message;
public $commit;
public function __construct($err,$code,$msg){
$this->error = $err;
$this->code = $code;
$this->message = $msg;
$this->url = Core::$CurrentPage;
$this->commit = Core::$Config["CommitID"];
}
}

44
src/core/init/loader.php Normal file
View file

@ -0,0 +1,44 @@
<?php
// require all necessary files
require ("core.php");
require ("user.php");
require ("responder.php");
require ("errors.php");
class Loader {
public function request_page(){
// Check if user is authenticated and go to the relevant section
// Currently no authentication is in place so should always be true
if (User::is_authenticated()){
$this->go_to_page(true);
} else {
$this->go_to_page(false);
}
}
public function go_to_page($authenticated) {
switch ($authenticated) {
case false:
require(Core::root_dir()."/public/unauthorized.php");
break;
default:
$p = $this->get_page_name();
if(substr($p,0,1) == "/") {
// Remove first slash if exists
Core::$CurrentPage = substr($p,1);
} else {
Core::$CurrentPage = $p;
}
require_once(Core::root_dir()."/public/respond.php");
break;
}
}
public function get_page_name(){
$uri = Core::get_get_data();
$page = "/none";
if(isset($uri["path"])){
$page = parse_url($uri["path"]);
$page = $page["path"];
}
return $page;
}
}

187
src/core/init/responder.php Normal file
View file

@ -0,0 +1,187 @@
<?php
class Responder {
private $Response;
public function show_response(){
// get the response detailed by the url requested
$response = $this->get_response();
if ($response){
// send response
$this->send_response($response);
} else {
// Send error instead
$e = new Errors();
$e->throw_error("NotFound");
}
}
private function send_response($response){
switch ($response->content_type){
case "json":
header('Content-Type: application/json');
// Send json encoded response
echo json_encode($response);
break;
case "ms-json":
header('Content-Type: application/json');
// Send json encoded response
// This is currently an undocumented file from Microsoft so it's not ready yet
echo json_encode($response->content, JSON_UNESCAPED_UNICODE);
break;
case "xml":
header('Content-Type: application/xml');
include ($response->content);
break;
}
}
private function get_response(){
$resp = false;
// Handle the requested URL, using as many known autoconfiguration urls as possible
switch (strtolower(Core::$CurrentPage)){
case "get/test":
$resp = $this->dummy_response();
break;
case "get/all":
$resp = $this->all_urls();
break;
case "mail/autoconfig.xml":
case "mail/config-v1.1.xml":
$resp = $this->moz_auto_config();
break;
case "autodiscover/autodiscover.xml":
$resp = $this->ms_autodiscover();
break;
case "autodiscover/autodiscover.json":
$resp = $this->ms_autodiscover_json();
break;
case "get/config":
$resp = $this->get_config();
break;
case "none":
case "test":
case "home":
case "root":
$resp = $this->get_test_working();
break;
default:
break;
}
return $resp;
}
private function get_config(){
$response = new Response();
$response->message = "Here's your config...";
foreach (Core::$Config as $k => $v){
$response->content[$k] = $v;
}
return $response;
}
private function get_username($service,$email_address) {
$username = "%EMAILADDRESS%";
if(!$service["UsernameIsFQDN"]) {
preg_match("/^[^@]+/",$email_address,$matches);
if (count($matches) > 0){
$username = $matches[0];
}
} else if ($service["RequireLogonDomain"]) {
$username = preg_replace("/[^@]+$/",Core::$Config["LogonDomain"],$email_address,1);
}
return $username;
}
private function all_urls(){
$response = new Response();
// Not really useful, unless some lovely app developers want to parse it for their app :)
// TODO:: Will work out a better message later
$response->message = "All URLs Requested";
// Cycle through each service and add to payload
foreach (Core::$Config["Services"] as $key => $service){
if (!$service["Enabled"]) {
continue;
}
$response->content[$key] = $service;
}
return $response;
}
private function moz_auto_config(){
// The default Thunderbird or Evolution autoconfig.xml file
$response = new Response();
$response->content_type = "xml";
$response->content = "public/autoconfig.php";
return $response;
}
private function ms_autodiscover(){
// The default Microsoft Autodiscover.xml file
$response = new Response();
$response->content_type = "xml";
$response->content = "public/autodiscover.php";
return $response;
}
private function ms_autodiscover_json(){
// The new Microsoft Autodiscover.json file - undocumented
$response = new Response();
$response->content_type = "ms-json";
if (strtolower($_GET['Protocol']) == 'autodiscoverv1') {
$response->content = new MSAutodiscoverJSONResponse();
$response->content->Protocol = "AutodiscoverV1";
$response->content->Url = Core::$Config["BaseURL"] . "/Autodiscover/Autodiscover.xml";
} else {
$response->content = new MSAutodiscoverJSONError();
http_response_code(400);
$response->headers_set = true;
$response->content->ErrorCode = "InvalidProtocol";
$response->content->ErrorMessage = "The given protocol value '" . $_GET['Protocol'] . "' is invalid. Supported values are 'AutodiscoverV1'";
}
return $response;
}
private function dummy_response(){
// Generate a dummy response for testing
$response = new Response();
$response->message = "OK, here's some scrumptious data! Enjoy!";
$response->content = array("data" => array("some_data" => "Ohhhhhmmmm nom nom nom nom nom nom",
"extra_data" => array("garnish" => "buuuuuuuuuuurp")),
"more_data" => "yuuuuuum yum yum yum");
return $response;
}
private function get_test_working(){
// Generate a dummy response for testing
$response = new Response();
$response->message = "Success! Things are working! Please request a valid URL i.e. /mail/config-v1.1.xml";
return $response;
}
}
class Response {
public $url;
public $content_type = "json";
public $message;
// public $headers_set = false;
public $content = array();
public function __construct(){
// add requested page to response. I don't know why, but it could helpful for diagnostics at some point
// Does not happen on autoconfig.xml/autodisocver.xml/autodiscover.json files
$this->url = Core::$CurrentPage;
if (!Core::$Config["CommitID"]){
$this->version = Core::VERSION;
} else {
$this->version = "git commit ".Core::$Config["CommitID"];
}
}
}
class AutoConfig {
public function prepare_autoconfig_info() {
// TODO: Move all the code in autoconfig.php into here
return false;
}
}
class MSAutodiscoverJSONResponse {
// More work to do - handling of MS Autodiscover.json requests
public $Protocol;
public $Url;
}
class MSAutodiscoverJSONError {
public $ErrorCode;
public $ErrorMessage;
}

9
src/core/init/user.php Normal file
View file

@ -0,0 +1,9 @@
<?php
class User {
public static function is_authenticated(){
// lots of work to do here to handle authentication.
// Currently just returns true for testing purposes
// I intend to make this authenticate against the primary IMAP server
return true;
}
}

View file

@ -0,0 +1,13 @@
; Sample config.ini file.
; Copy this file to "config/config.ini" and adjust the settings to your requirements
; The URL of this application
BaseURL = "https://autoconfig.example.com"
; Set the email domains for use with this service. The first one is primary.
; each will need their own DNS A record for autoconfig.domain.name
Domain[] = example.com
Domain[] = example2.com
; If you use a different domain to authenticate with, enter it here
LogonDomain = example.local

View file

@ -1,17 +0,0 @@
---
Version : "0.1.5"
# Sample config.yaml file.
# Copy this file to "config/config.yaml" and adjust the
# settings to your requirements
# The URL of this application
BaseURL : "https://autoconfig.example.com"
# Set the email domains for use with this service. The first one is primary.
# each will need their own DNS A record for autoconfig.domain.name
Domains :
- "example.com"
- "example2,com"
# If you use a different domain to authenticate with, enter it here
LocalDomain : "example.local"

View file

@ -0,0 +1,116 @@
; The Incoming mail Server Config
[InMail]
; Enable this service
Enabled = true
; Mail Type, i.e. IMAP, POP3
Type = "IMAP"
; Your IMAP server
Server = "imap.example.com"
; Your IMAP port
Port = 993
; The socket type : SSL, STARTTLS, or NONE
SocketType = SSL
; Use Secure Password Authentication
SPA = false
; Change to true if you need the domain/logondomain to form part of the username
UsernameIsFQDN = false
; Do you need to authenticate to your mail server? You should! so this should be false!
NoAuthRequired = false
; Authentication type,
;"password-cleartext" : Send password in the clear
; (dangerous, if SSL isn't used either).
; AUTH PLAIN, LOGIN or protocol-native login.
;"password-encrypted" : A secure encrypted password mechanism.
; Can be CRAM-MD5 or DIGEST-MD5. Not NTLM.
;"NTLM": Use NTLM (or NTLMv2 or successors),
; the Windows login mechanism.
Authentication = password-cleartext
; The Outgoing mail server config
[OutMail]
; Enable this service
Enabled = true
; Mail type, likely to only be SMTP
Type = "SMTP"
; Your SMTP server
Server = "smtp.example.com"
; Your SMTP port
Port = 465
; The socket type : SSL, STARTTLS or NONE
SocketType = SSL
; See InMail > Authentication
Authentication = password-cleartext
; Use Secure Password Authentication
SPA = false
; Change to true if you need the domain/logondomain to form part of the username
UsernameIsFQDN = false
; Do you need to authenticate to your mail server? You should! so this should be false!
NoAuthRequired = false
; Use POP Authentication? You probably shouldn't be.
POPAuth = false
; This setting is here to limit errors, I'm not sure what it does yet.
SMTPLast = false
; Currently not implemented, see https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
[Calendar]
; Disable this service
Enabled = false
Server = "https://example.com/remote.php/dav/"
Port = 443
Type = CalDAV
Authentication = http-basic
UsernameIsFQDN = false
; Currently not implemented, see https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
[AddressBook]
; Disable this service
Enabled = false
Server = "https://example.com/remote.php/dav/"
Port = 443
Type = CardDAV
Authentication = http-basic
UsernameIsFQDN = false
; Currently not implemented, see https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
[WebMail]
; Disable this service
Enabled = false
Server = https://mail.example.com
UsernameDivID = "username"
UsernameDivName = "username"
PasswordDivName = "password"
SubmitButtonID = "submit"
SubmitButtonName = "submit"
UsernameIsFQDN = false
; In theory, additional custom services can be configured and will be displayed with
; their options on the /get/all URL of this service. The third-party clients would need to
; check this service as part of their development for this to work
; Will not be shown in autodiscover.xml/json or config-v1.1.xml/autoconfig.xml
; i.e Nextcloud - ideally a nextcloud client could check autoconfig for this URL for ease of set up
;[Nextcloud]
;Server = https://nextcloud.example.com

View file

@ -1,133 +0,0 @@
# Sample services.yaml file.
# Copy this file to "config/services.yaml" and adjust the
# settings to your requirements
---
# The Incoming mail Server Config
InMail:
Name: "InMail"
# Enable this service
Enabled: true
# Mail Type, i.e. IMAP, POP3
Type: "IMAP"
# Your IMAP server
Server: "imap.example.com"
# Your IMAP port
Port: 993
# The socket type : SSL, STARTTLS, or NONE
SocketType: "SSL"
# Use Secure Password Authentication
SPA: false
# Change to true if you need the domain/logondomain to form part of the username
UsernameIsFQDN: false
# Use the LogonDomain instead of the Email Domain
RequireLocalDomain : false
# Do you need to authenticate to your mail server? You should! so this should be false!
NoAuthRequired: false
# Authentication type,
#"password-cleartext" : Send password in the clear
# (dangerous, if SSL isn't used either).
# AUTH PLAIN, LOGIN or protocol-native login.
#"password-encrypted" : A secure encrypted password mechanism.
# Can be CRAM-MD5 or DIGEST-MD5. Not NTLM.
#"NTLM": Use NTLM (or NTLMv2 or successors),
# the Windows login mechanism.
Authentication: "password-cleartext"
# The Outgoing mail server config
OutMail:
# Enable this service
Enabled: true
# Mail type, likely to only be SMTP
Type: "SMTP"
# Your SMTP server
Server: "smtp.example.com"
# Your SMTP port
Port: 465
# The socket type : SSL, STARTTLS or NONE
SocketType: "SSL"
# See InMail > Authentication
Authentication: "password-cleartext"
# Use Secure Password Authentication
SPA: false
# Change to true if you need the domain/logondomain to form part of the username
UsernameIsFQDN: false
# Use the LogonDomain instead of the Email Domain
RequireLocalDomain : false
# Do you need to authenticate to your mail server? You should! so this should be false!
NoAuthRequired: false
# Use POP Authentication? You probably shouldn't be.
POPAuth: false
# This setting is here to limit errors, I'm not sure what it does yet.
SMTPLast: false
# Currently not implemented, see https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
Calendar:
Name: "Calendar"
# Disable this service
Enabled: false
Server: "https://example.com/remote.php/dav/"
Port: 443
Type: "CalDAV"
Authentication: "http-basic"
UsernameIsFQDN: false
RequireLocalDomain : false
# Currently not implemented, see https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
AddressBook:
Name: "AddressBook"
# Disable this service
Enabled: false
Server: "https://example.com/remote.php/dav/"
Port: 443
Type: "CardDAV"
Authentication: "http-basic"
UsernameIsFQDN: false
RequireLocalDomain : false
# Currently not implemented, see https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
WebMail:
Name: "WebMail"
# Disable this service
Enabled: false
Server: "https://mail.example.com"
UsernameDivID: "username"
UsernameDivName: "username"
PasswordDivName: "password"
SubmitButtonID: "submit"
SubmitButtonName: "submit"
UsernameIsFQDN: false
RequireLocalDomain : false
# In theory, additional custom services can be configured and will be displayed with
# their options on the /get/all URL of this service. The third-party clients would need to
# check this service as part of their development for this to work
# Will not be shown in autodiscover.xml/json or config-v1.1.xml/autoconfig.xml
# i.e Nextcloud - ideally a nextcloud client could check autoconfig for this URL for ease of set up
#OtherServices:
# -
# Name : "NextCloud"
# Server : "https://nextcloud.example.com"

View file

@ -1,157 +0,0 @@
package global
import (
. "mailautoconf/global/structs"
"mailautoconf/global/logger"
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
// "os"
"encoding/json"
"text/template"
"path"
"regexp"
"strings"
"time"
// "reflect"
)
// Global variables
var ThisSession Session
var MainConfig Config
// Template declaration
var templates []string = []string{"autodiscover.xml","autoconfig.xml"}
var Templates map[string]*template.Template = make(map[string]*template.Template)
const defaultConfigDir string = "default-config/"
const configDir string = "config/"
func NewSessionID() string{
timecode := time.Now()
id := timecode.Format("20060102150405.000")
id = strings.Replace(id,".","",1)
return id
}
func NewConfig() Config {
MainConfig = loadConfig()
loadXMLTemplates()
return MainConfig
}
func loadConfig() Config {
cfg := Config{}
logger.Log("Loading Default Config…")
cfgfile := defaultConfigDir + "config.default.yaml"
unmarshalConfig(cfgfile, &cfg)
logger.Log("Loading Custom Config…")
customcfgfile := configDir + "config.yaml"
unmarshalConfig(customcfgfile, &cfg)
logger.Log("Loading Default Services…")
svcfile := defaultConfigDir + "services.default.yaml"
unmarshalConfig(svcfile, &cfg)
logger.Log("Loading Custom Services…")
customsvcfile := configDir + "services.yaml"
unmarshalConfig(customsvcfile, &cfg)
removeDisabledItems(&cfg)
return cfg
}
func loadXMLTemplates(){
for _, tmpl := range templates {
tmpl := fmt.Sprintf("templates/%s",tmpl)
name := path.Base(tmpl)
var fmap = template.FuncMap{
"lower": strings.ToLower,
"parseUsername": parseUsername,
"onoff": chooseOnOff,
}
t, err := template.New(name).Funcs(fmap).ParseFiles(tmpl)
logger.CheckError(err)
Templates[name] = t
}
}
func unmarshalConfig(file string, cfg *Config) {
if logger.FileExists(file) {
content, err := ioutil.ReadFile(file)
if !logger.ErrorOK(err){
logger.Log("Error reading config :", file, " : ", fmt.Sprintf("%v",err))
}
err2 := yaml.Unmarshal(content, &cfg)
if !logger.ErrorOK(err2){
logger.Log("Error unmarshaling config :", file, " : ", fmt.Sprintf("%v",err2))
}
}
}
func removeDisabledItems(cfg *Config) {
// Rework this, not pretty
if !cfg.InMail.Enabled {
cfg.InMail = Service{}
}
if !cfg.OutMail.Enabled {
cfg.OutMail = Service{}
}
if !cfg.Calendar.Enabled {
cfg.Calendar = Service{}
}
if !cfg.AddressBook.Enabled {
cfg.AddressBook = Service{}
}
if !cfg.WebMail.Enabled {
cfg.WebMail = Service{}
}
new_svcs := []Service{}
for _,svc := range cfg.OtherServices {
if svc.Enabled {
new_svcs = append(new_svcs,svc)
}
}
cfg.OtherServices = new_svcs
}
func JSONifyConfig(content Config) string {
data, err := json.Marshal(content)
logger.CheckError(err)
return string(data)
}
func JSONify(content interface{}) string {
data, err := json.Marshal(content)
logger.CheckError(err)
return string(data)
}
func parseUsername(svc Service, email string) string {
if email == "" {
return "not-provided"
}
if svc.UsernameIsFQDN && !svc.RequireLocalDomain{
return email
} else if svc.UsernameIsFQDN && svc.RequireLocalDomain {
re := regexp.MustCompile(`[^@(%40)]+$`)
domain := re.FindString(email)
localemail := strings.Replace(email, domain,
MainConfig.LocalDomain,1)
return localemail
} else {
re := regexp.MustCompile(`^[^@(%40)]+`)
username := re.FindString(email)
return username
}
}
func chooseOnOff(value bool) string {
if value {
return "on"
} else {
return "off"
}
}
// GetIP gets a requests IP address by reading off the forwarded-for
// header (for proxies) and falls back to use the remote address.
func GetSessionIP() string {
r := ThisSession.Request
ip := r.RemoteAddr
forwarded := r.Header.Get("X-FORWARDED-FOR")
if forwarded != "" {
ip = forwarded
}
logger.Log("Session ", ThisSession.ID, " Connect From : ", ip)
return ip
}

View file

@ -1,73 +0,0 @@
package logger
import (
"log"
"fmt"
"io/ioutil"
"os"
"time"
)
const logDir = "config/logs"
func Log(str ...string) {
makeLogDir()
line := ""
for _, s := range str {
line = line + s
}
line = line + "\r\n"
log.Print(line)
t := time.Now()
logname := fmt.Sprintf("%s_log.log",t.Format("200601"))
logfile := fmt.Sprintf("%s/%s",logDir, logname)
line = fmt.Sprintf("%s %s",t.Format("2006/01/02 15:04:05"), line)
if !FileExists(logfile) {
err := ioutil.WriteFile(logfile, []byte(line), 0755)
CheckError(err)
} else {
file, err := os.OpenFile(logfile, os.O_APPEND|os.O_WRONLY, 0755)
CheckError(err)
defer file.Close()
if _, err := file.WriteString(line); err != nil {
CheckError(err)
}
}
}
func CheckError(err error) (ok bool) {
// here for obsolescence
return ErrorOK(err)
}
func ErrorOK(err error) (ok bool) {
ok = true // All is OK, err == nil
if err != nil {
ok = false // There's an error, print it
e := fmt.Sprintf("%v",err)
Log(e)
}
return
}
func Fatal(err error) {
e := fmt.Sprintf("%v",err)
Log(e)
log.Fatal(err)
}
func makeLogDir(){
_, err := os.Stat(logDir)
if os.IsNotExist(err) {
os.Mkdir(logDir, 0755)
}
}
func FileExists(file string) bool {
exists := false
_, err := os.Stat(file);
if os.IsNotExist(err) {
log.Print("File does not exist : ", file);
} else if err == nil {
exists = true
} else {
log.Fatal(err)
log.Print("File %s does not exist\n", file);
}
return exists
}

View file

@ -1,67 +0,0 @@
package structs
import "net/http"
type Session struct {
ID string
IP string
ResponseWriter http.ResponseWriter
Request *http.Request
Path string
WebContent string
ContentType string
}
type Config struct {
Version string `yaml:"Version"`
BaseURL string `yaml:"BaseURL"`
Domains []string `yaml:"Domains"`
LocalDomain string `yaml:"LocalDomain"`
InMail Service `yaml:"InMail" json:",omitempty"`
OutMail Service `yaml:"OutMail" json:",omitempty"`
Calendar Service `yaml:"Calendar" json:",omitempty"`
AddressBook Service `yaml:"AddressBook" json:",omitempty"`
WebMail Service `yaml:"WebMail" json:",omitempty"`
OtherServices []Service `yaml:"OtherServices" json:",omitempty"`
}
type Service struct {
Name string `yaml:"Name" json:",omitempty"`
Enabled bool `yaml:"Enabled" json:",omitempty"`
Type string `yaml:"Type" json:",omitempty"`
Server string `yaml:"Server" json:",omitempty"`
Port int `yaml:"Port" json:",omitempty"`
SocketType string `yaml:"SocketType" json:",omitempty"`
SPA bool `yaml:"SPA" json:",omitempty"`
UsernameIsFQDN bool `yaml:"UsernameIsFQDN" json:",omitempty"`
RequireLocalDomain bool `yaml:"RequireLocalDomain" json:",omitempty"`
NoAuthRequired bool `yaml:"NoAuthRequired" json:",omitempty"`
Authentication string `yaml:"Authentication" json:",omitempty"`
// For Outgoing Mail
POPAuth bool `yaml:"POPAuth" json:",omitempty"`
SMTPLast bool `yaml:"SMTPLast" json:",omitempty"`
// For WebMail (Unused)
UsernameDivID string `yaml:"UsernameDivID" json:",omitempty"`
UsernameDivName string `yaml:"UsernameDivName" json:",omitempty"`
PasswordDivName string `yaml:"PasswordDivName" json:",omitempty"`
SubmitButtonID string `yaml:"SubmitButtonID" json:",omitempty"`
SubmitButtonName string `yaml:"SubmitButtonName" json:",omitempty"`
}
type Response struct {
Url string `json:"url"`
ContentType string `json:"content_type"`
Message string `json:"message"`
Content map[string]interface{} `json:"content,omitempty"`
Config Config `json:"_"`
Email string `json:"_"`
}
type MSAutodiscoverJSONResponse struct {
// More work to do - handling of MS Autodiscover.json requests
Protocol string
Url string
}
type MSAutodiscoverJSONError struct{
ErrorCode string
ErrorMessage string
}

View file

@ -1,15 +0,0 @@
module mailautoconf
go 1.16
require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

View file

@ -1,28 +0,0 @@
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e h1:VvfwVmMH40bpMeizC9/K7ipM5Qjucuu16RWfneFPyhQ=
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

5
src/index.php Normal file
View file

@ -0,0 +1,5 @@
<?php
// entry point - inport and run the public index.php file
include("core/init.php");
$b = new Init();
$b->start();

View file

@ -1,17 +0,0 @@
package main
import (
// "fmt"
"net/http"
// "log"
"mailautoconf/web/handler"
"mailautoconf/global"
"mailautoconf/global/logger"
)
func main() {
global.NewConfig()
http.HandleFunc("/", handler.WebHandler)
logger.Log("Starting up Web Listener on port 8010")
logger.Fatal(http.ListenAndServe(":8010", nil))
}

66
src/public/autoconfig.php Normal file
View file

@ -0,0 +1,66 @@
<?php
// Get some core information to help with generation.
$services = Core::$Config["Services"];
$data = Core::get_get_data();
if (isset($data["emailaddress"])) {
$email_address = $data["emailaddress"];
}
// The below link has config-v1.1.xml information
// https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
?>
<clientConfig version="1.1">
<emailProvider id="<?php echo Core::$Config["PrimaryDomain"]?>">
<?php foreach (Core::$Config["Domain"] as $domain){ ?>
<domain><?php echo $domain; ?></domain>
<?php } ?>
<displayName>%EMAILADDRESS%</displayName>
<?php if($services["InMail"]&& $services["InMail"]["Enabled"]){
$service = $services["InMail"]; ?>
<incomingServer type="<?php echo strtolower($service["Type"]);?>">
<hostname><?php echo $service["Server"];?></hostname>
<port><?php echo $service["Port"];?></port>
<socketType><?php echo $service["SocketType"];?></socketType>
<username><?php echo $this->get_username($service,$email_address); ?></username>
<authentication><?php echo $service["Authentication"];?></authentication>
</incomingServer>
<?php }
if($services["OutMail"]&& $services["OutMail"]["Enabled"]){
$service = $services["OutMail"]; ?>
<outgoingServer type="<?php echo strtolower($service["Type"]);?>">
<hostname><?php echo $service["Server"];?></hostname>
<port><?php echo $service["Port"];?></port>
<socketType><?php echo $service["SocketType"];?></socketType>
<username><?php echo $this->get_username($service,$email_address);?></username>
<authentication><?php echo $service["Authentication"];?></authentication>
</outgoingServer>
<?php }
if ($services["AddressBook"] && $services["AddressBook"]["Enabled"]) {
$service = $services["AddressBook"]; ?>
<addressBook type="<?php echo strtolower($service["Type"]); ?>">
<username><?php echo $this->get_username($service,$email_address);?></username>
<authentication><?php echo $service["Authentication"];?></authentication>
<serverURL><?php echo $service["Server"];?></serverURL>
</addressBook>
<?php }
if ($services["Calendar"] && $services["Calendar"]["Enabled"]){
$service = $services["Calendar"] ;?>
<calendar type="<?php echo strtolower($service["Type"]);?>">
<username><?php echo $this->get_username($service,$email_address);?></username>
<authentication><?php echo $service["Authentication"];?></authentication>
<serverURL><?php echo $service["Server"];?></serverURL>
</calendar>
<?php }
if ($services["WebMail"] && $services["WebMail"]["Enabled"]) {
$service = $services["WebMail"]; ?>
<webMail>
<loginPage url="<?php echo $service["Server"];?>" />
<loginPageInfo url="<?php echo $service["Server"];?>">
<username><?php echo $this->get_username($service,$email_address);?></username>
<usernameField id="<?php echo $service["UsernameDivID"];?>" name="<?php echo $service["UsernameDivName"];?>" />
<passwordField name="<?php echo $service["PasswordDivName"];?>" />
<loginButton id="<?php echo $service["SubmitButtonID"];?>" name="<?php echo $service["SubmitButtonName"];?>"/>
</loginPageInfo>
</webMail>
<?php } ?>
</emailProvider>
</clientConfig>

View file

@ -0,0 +1,52 @@
<?php
$conf = Core::$Config["Services"];
//get raw POST data so we can extract the email address
$data = file_get_contents("php://input");
preg_match("/\<EMailAddress\>(.*?)\<\/EMailAddress\>/", $data, $matches);
// Example POST Request (sent from client) :
// <?xml version="1.0" \?\>
// <Autodiscover xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
// <Request>
// <EMailAddress>your@email.address</EMailAddress>
// <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
// </Request>
// </Autodiscover>
echo '<?xml version="1.0" encoding="utf-8" ?>';?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<Account>
<AccountType>email</AccountType>
<Action>settings</Action>
<?php if ($conf["InMail"] && $conf["InMail"]["Enabled"]){
$in = $conf["InMail"];?>
<Protocol>
<Type><?php echo $in["Type"];?></Type>
<Server><?php echo $in["Server"];?></Server>
<Port><?php echo $in["Port"];?></Port>
<DomainRequired><?php echo Core::$Config["RequireAuthDomain"] ? "on" : "off";?></DomainRequired>
<LoginName><?php echo isset($matches[1]) ? $matches[1] : false ; ?></LoginName>
<SPA><?php echo $in["SPA"] ? "on" : "off";?></SPA>
<SSL><?php echo $in["SocketType"] == "SSL" ? "on" : "off";?></SSL>
<AuthRequired><?php echo $in["NoAuthRequired"] ? "off" : "on";?></AuthRequired>
</Protocol>
<?php }
if ($conf["OutMail"]&& $conf["OutMail"]["Enabled"]) {
$out = $conf["OutMail"];?>
<Protocol>
<Type><?php echo $out["Type"];?></Type>
<Server><?php echo $out["Server"];?></Server>
<Port><?php echo $out["Port"];?></Port>
<DomainRequired><?php echo Core::$Config["RequireAuthDomain"] ? "on" : "off";?></DomainRequired>
<LoginName><?php echo isset($matches[1]) ? $matches[1] : false ; ?></LoginName>
<SPA><?php echo $in["SPA"] ? "on" : "off";?></SPA>
<Encryption><?php echo $in["SocketType"];?></Encryption>
<AuthRequired><?php echo $in["NoAuthRequired"] ? "off" : "on";?></AuthRequired>
<UsePOPAuth><?php echo $in["POPAuth"] ? "on" : "off";?></UsePOPAuth>
<SMTPLast><?php echo $in["SMTPLast"] ? "on" : "off";?></SMTPLast>
</Protocol>
<?php } ?>
</Account>
</Response>
</Autodiscover>

5
src/public/error.php Normal file
View file

@ -0,0 +1,5 @@
<?php
//return the json response :
$e = new Errors();
$e->throw_error("NotFound");
exit();

5
src/public/respond.php Normal file
View file

@ -0,0 +1,5 @@
<?php
//return the json response :
$e = new Responder();
$e->show_response();
exit();

View file

@ -0,0 +1,5 @@
<?php
//return the json response :
$e = new Errors();
$e->throw_error("Unauthorized");
exit();

View file

@ -1,60 +0,0 @@
<clientConfig version="1.1">
<emailProvider id="{{ index .Config.Domains 0 }}">
{{ range .Config.Domains }}<domain>{{ . }}</domain>
{{ end }}
<displayName>{{ .Email }}</displayName>
{{ with .Config.InMail }}
{{ if .Enabled }}
<incomingServer type="{{ .Type | lower }}">
<hostname>{{ .Server }}</hostname>
<port>{{ .Port }}</port>
<socketType>{{ .SocketType }}</socketType>
<username>{{ $.Email | parseUsername . }}</username>
<authentication>{{ .Authentication }}</authentication>
</incomingServer>
{{ end }}
{{ end }}
{{ with .Config.OutMail }}
{{ if .Enabled }}
<outgoingServer type="{{ .Type | lower }}">
<hostname>{{ .Server }}</hostname>
<port>{{ .Port }}></port>
<socketType>{{ .SocketType }}</socketType>
<username>{{ $.Email | parseUsername . }}</username>
<authentication>{{ .Authentication }}</authentication>
</outgoingServer>
{{ end }}
{{ end }}
{{ with .Config.AddressBook }}
{{ if .Enabled }}
<addressBook type="{{ .Type | lower }}">
<username>{{ $.Email | parseUsername . }}</username>
<authentication>{{ .Authentication }}</authentication>
<serverURL>{{ .Server }}addressbooks/users/{{ $.Email | parseUsername . }}/contacts/</serverURL>
</addressBook>
{{ end }}
{{ end }}
{{ with .Config.Calendar }}
{{ if .Enabled }}
<calendar type="{{ .Type | lower }}">
<username>{{ $.Email | parseUsername . }}</username>
<authentication>{{ .Authentication }}</authentication>
<serverURL>{{ .Server }}calendars/{{ $.Email | parseUsername . }}/personal/</serverURL>
</calendar>
{{ end }}
{{ end }}
{{ with .Config.WebMail }}
{{ if .Enabled }}
<webMail>
<loginPage url="{{ .Server }}" />
<loginPageInfo url="{{ .Server }}">
<username>{{ $.Email | parseUsername . }}</username>
<usernameField id="{{ .UsernameDivID }}" name="{{ .UsernameDivID }}" />
<passwordField name="{{ .PasswordDivName }}" />
<loginButton id="{{ .SubmitButtonID }}" name="{{ .SubmitButtonName }}"/>
</loginPageInfo>
</webMail>
{{ end }}
{{ end }}
</emailProvider>
</clientConfig>

View file

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<Account>
<AccountType>email</AccountType>
<Action>settings</Action>
{{ with .Config.InMail }}
{{ if .Enabled }}
<Protocol>
<Type>{{ .Type }}</Type>
<Server>{{ .Server }}</Server>
<Port>{{ .Port }}</Port>
<DomainRequired>{{ .UsernameIsFQDN | onoff }}</DomainRequired>
<LoginName>{{ $.Email | parseUsername . }}</LoginName>
<SPA>{{ .SPA | onoff }}</SPA>
<SSL>{{ if eq .SocketType "SSL" }}on{{ else }}off{{ end }}</SSL>
<AuthRequired>{{ not .NoAuthRequired | onoff }}</AuthRequired>
</Protocol>
{{ end }}
{{ end }}
{{ with .Config.OutMail }}
{{ if .Enabled }}
<Protocol>
<Type>{{ .Type }}</Type>
<Server>{{ .Server }}</Server>
<Port>{{ .Port }}</Port>
<DomainRequired>{{ .UsernameIsFQDN | onoff }}</DomainRequired>
<LoginName>{{ $.Email | parseUsername . }}</LoginName>
<SPA>{{ .SPA | onoff }}</SPA>
<Encryption>{{ .SocketType }}</Encryption>
<AuthRequired>{{ not .NoAuthRequired | onoff }}</AuthRequired>
<UsePOPAuth>{{ .POPAuth | onoff }}</UsePOPAuth>
<SMTPLast>{{ .SMTPLast | onoff }}</SMTPLast>
</Protocol>
{{ end }}
{{ end }}
</Account>
</Response>
</Autodiscover>

View file

@ -1,44 +0,0 @@
package handler
import (
. "mailautoconf/global"
. "mailautoconf/global/structs"
"mailautoconf/global/logger"
"mailautoconf/web/responses"
"strings"
"net/http"
"fmt"
)
func WebHandler(w http.ResponseWriter, r *http.Request) {
ThisSession = Session{}
ThisSession.ResponseWriter = w
ThisSession.Request = r
ThisSession.ID = NewSessionID()
url := fmt.Sprintf("%s", r.URL)
logger.Log("Session ", ThisSession.ID, " Request For : ", url )
ThisSession.IP = GetSessionIP()
ThisSession.Path = strings.ToLower(r.URL.Path[1:])
if ThisSession.Path == "" {
ThisSession.Path = "none"
}
switch ThisSession.Path {
case "mail/config-v1.1.xml",
"mail/autoconfig.xml":
ThisSession.WebContent = responses.MozAutoconfig()
case "autodiscover/autodiscover.xml":
ThisSession.WebContent = responses.MsAutoDiscoverXML()
case "autodiscover/autodiscover.json":
ThisSession.WebContent = responses.MsAutoDiscoverJSON()
case "get/config":
ThisSession.WebContent = responses.OurConfig()
default:
ThisSession.WebContent = responses.DefaultResponse()
}
webOutput()
}
func webOutput(){
ThisSession.ResponseWriter.Header().Set("Content-Type", ThisSession.ContentType)
fmt.Fprintf(ThisSession.ResponseWriter, ThisSession.WebContent)
}

View file

@ -1,115 +0,0 @@
package responses
import (
"mailautoconf/global"
"mailautoconf/global/logger"
. "mailautoconf/global/structs"
// "text/template"
"fmt"
// "path"
"strings"
"bytes"
"regexp"
)
var email string
func MozAutoconfig() string {
// The below link has config-v1.1.xml information
// https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
// parse the querystring
logger.CheckError(global.ThisSession.Request.ParseForm())
// build the response
response := Response{}
response.Email = global.ThisSession.Request.FormValue("emailaddress")
email = response.Email
response.Config = global.MainConfig
// set content type to XML
global.ThisSession.ContentType = "application/xml"
// execute the template
var result bytes.Buffer
template := global.Templates["autoconfig.xml"]
err := template.Execute(&result, response)
logger.CheckError(err)
// return our string of xml
return result.String()
}
func MsAutoDiscoverXML() string {
// MS Outlook Autodiscover.xml
//
// Example POST Request (sent from client) :
// <?xml version="1.0" \?\>
// <Autodiscover xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
// <Request>
// <EMailAddress>your@email.address</EMailAddress>
// <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
// </Request>
// </Autodiscover>
// Parse the form to get the values
logger.CheckError(global.ThisSession.Request.ParseForm())
// convert the input to a string so we can extract the email address
form := fmt.Sprintf("%s",global.ThisSession.Request.Form)
// fine the EMailAddress section
find := regexp.MustCompile(`\<EMailAddress\>(.*?)\<\/EMailAddress\>`)
email = find.FindString(form)
// replace the tags
replace := regexp.MustCompile(`\<[\/]?EMailAddress\>`)
email = replace.ReplaceAllString(email,``)
logger.Log("Session ",global.ThisSession.ID ," Request for email : ",email)
// build the reponse
response := Response{}
response.Email = email
response.Config = global.MainConfig
// execute the template
template := global.Templates["autodiscover.xml"]
global.ThisSession.ContentType = "application/xml"
var result bytes.Buffer
err := template.Execute(&result, response)
logger.CheckError(err)
// return our string of xml
return result.String()
}
func MsAutoDiscoverJSON() string {
// MS Outlook Autodiscover.json - undocumented
//
// Example Request
// /autodiscover/autodiscover.json?Email=you@your.domain&Protocol=Autodiscoverv1&RedirectCount=1
email = global.ThisSession.Request.FormValue("Email")
protocol := global.ThisSession.Request.FormValue("Protocol")
global.ThisSession.ContentType = "application/json"
switch strings.ToLower(protocol) {
case "autodiscoverv1":
response := MSAutodiscoverJSONResponse{}
response.Protocol = "AutodiscoverV1"
response.Url = fmt.Sprintf("%s/Autodiscover/Autodiscover.xml", global.MainConfig.BaseURL)
return global.JSONify(response)
default:
response := MSAutodiscoverJSONError{}
response.ErrorCode = "InvalidProtocol";
response.ErrorMessage = fmt.Sprintf("The given protocol value '%s' is invalid. Supported values are 'AutodiscoverV1'", protocol)
return global.JSONify(response)
}
}
func DefaultResponse() string {
response := Response{}
response.Url = global.ThisSession.Path
global.ThisSession.ContentType = "application/json"
response.ContentType = global.ThisSession.ContentType
response.Message = "Success! Things are working! Please request a valid URL i.e. /mail/config-v1.1.xml";
return global.JSONify(response)
}
func OurConfig() string {
global.ThisSession.ContentType = "application/json"
content := global.JSONifyConfig(global.MainConfig)
return content
}

4
test-entry.sh Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
a2enmod rewrite
service apache2 stop
exec bash /entrypoint.sh

11
test-server.sh Normal file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
podman run --name mailautoconf-test \
--rm \
-p "8010:80" \
-v ./src:/var/www/html/ \
-v ./config:/var/www/html/config \
-v ./test-entry.sh:/test-entry.sh \
-v ./entrypoint.sh:/entrypoint.sh \
--entrypoint "/bin/bash" \
php:7.4-apache \
/test-entry.sh