Compare commits

...

11 commits

10 changed files with 196 additions and 134 deletions

View file

@ -1,7 +1,11 @@
# 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! ## New GoLang version - please make sure you update your ini files to yaml!
Github: https://github.com/pswilde/mailautoconf 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
@ -60,7 +64,7 @@ SRV _autodiscover._tcp.your.domain 3600 10 10 443 autoconfig.your.domain
``` ```
## Compatibility ## Compatibility
MailAutoConf has been tested and confirmed working with the following software packages MailAutoConf has been tested and confirmed working (for IMAP and SMTP) 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
@ -86,8 +90,11 @@ 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?
Well, not yet. Though it does sort of work already. It works for non-Microsoft email clients now (see Compatibility above).
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. 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.
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

@ -1,5 +1,5 @@
--- ---
Version : "0.1.3" Version : "0.1.5"
# Sample config.yaml file. # Sample config.yaml file.
# Copy this file to "config/config.yaml" and adjust the # Copy this file to "config/config.yaml" and adjust the
# settings to your requirements # settings to your requirements

View file

@ -1,10 +1,11 @@
package global package global
import ( import (
. "mailautoconf/structs" . "mailautoconf/global/structs"
"mailautoconf/global/logger"
"fmt" "fmt"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"os" // "os"
"encoding/json" "encoding/json"
"text/template" "text/template"
"path" "path"
@ -39,16 +40,16 @@ func NewConfig() Config {
} }
func loadConfig() Config { func loadConfig() Config {
cfg := Config{} cfg := Config{}
fmt.Println("Loading Default Config…") logger.Log("Loading Default Config…")
cfgfile := defaultConfigDir + "config.default.yaml" cfgfile := defaultConfigDir + "config.default.yaml"
unmarshalConfig(cfgfile, &cfg) unmarshalConfig(cfgfile, &cfg)
fmt.Println("Loading Custom Config…") logger.Log("Loading Custom Config…")
customcfgfile := configDir + "config.yaml" customcfgfile := configDir + "config.yaml"
unmarshalConfig(customcfgfile, &cfg) unmarshalConfig(customcfgfile, &cfg)
fmt.Println("Loading Default Services…") logger.Log("Loading Default Services…")
svcfile := defaultConfigDir + "services.default.yaml" svcfile := defaultConfigDir + "services.default.yaml"
unmarshalConfig(svcfile, &cfg) unmarshalConfig(svcfile, &cfg)
fmt.Println("Loading Custom Services…") logger.Log("Loading Custom Services…")
customsvcfile := configDir + "services.yaml" customsvcfile := configDir + "services.yaml"
unmarshalConfig(customsvcfile, &cfg) unmarshalConfig(customsvcfile, &cfg)
removeDisabledItems(&cfg) removeDisabledItems(&cfg)
@ -64,21 +65,19 @@ func loadXMLTemplates(){
"onoff": chooseOnOff, "onoff": chooseOnOff,
} }
t, err := template.New(name).Funcs(fmap).ParseFiles(tmpl) t, err := template.New(name).Funcs(fmap).ParseFiles(tmpl)
if err != nil { logger.CheckError(err)
panic (err)
}
Templates[name] = t Templates[name] = t
} }
} }
func unmarshalConfig(file string, cfg *Config) { func unmarshalConfig(file string, cfg *Config) {
if FileExists(file) { if logger.FileExists(file) {
content, err := ioutil.ReadFile(file) content, err := ioutil.ReadFile(file)
if err != nil { if !logger.ErrorOK(err){
fmt.Println("Error reading config :", file, " : ", err) logger.Log("Error reading config :", file, " : ", fmt.Sprintf("%v",err))
} }
err2 := yaml.Unmarshal(content, &cfg) err2 := yaml.Unmarshal(content, &cfg)
if err2 != nil { if !logger.ErrorOK(err2){
fmt.Println("Error unmarshaling config :", file, " : ", err2) logger.Log("Error unmarshaling config :", file, " : ", fmt.Sprintf("%v",err2))
} }
} }
} }
@ -108,22 +107,15 @@ func removeDisabledItems(cfg *Config) {
} }
cfg.OtherServices = new_svcs cfg.OtherServices = new_svcs
} }
func FileExists(file string) bool {
exists := false
if _, err := os.Stat(file); err == nil {
exists = true
} else {
fmt.Println(err)
fmt.Printf("File %s does not exist\n", file);
}
return exists
}
func JSONifyConfig(content Config) string {
data, err := json.Marshal(content)
logger.CheckError(err)
return string(data)
}
func JSONify(content interface{}) string { func JSONify(content interface{}) string {
data, err := json.Marshal(content) data, err := json.Marshal(content)
if err != nil { logger.CheckError(err)
fmt.Println(err)
}
return string(data) return string(data)
} }
func parseUsername(svc Service, email string) string { func parseUsername(svc Service, email string) string {
@ -160,6 +152,6 @@ func GetSessionIP() string {
if forwarded != "" { if forwarded != "" {
ip = forwarded ip = forwarded
} }
fmt.Printf("Session %s Connect From : %s\r\f",ThisSession.ID, ip) logger.Log("Session ", ThisSession.ID, " Connect From : ", ip)
return ip return ip
} }

View file

@ -0,0 +1,73 @@
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

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

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"`
OutMail Service `yaml:"OutMail"`
Calendar Service `yaml:"Calendar"`
AddressBook Service `yaml:"AddressBook"`
WebMail Service `yaml:"WebMail"`
OtherServices []Service `yaml:"OtherServices"`
}
type Service struct {
Name string `yaml:"Name"`
Enabled bool `yaml:"Enabled"`
Type string `yaml:"Type"`
Server string `yaml:"Server"`
Port int `yaml:"Port"`
SocketType string `yaml:"SocketType"`
SPA bool `yaml:"SPA"`
UsernameIsFQDN bool `yaml:"UsernameIsFQDN"`
RequireLocalDomain bool `yaml:"RequireLocalDomain"`
NoAuthRequired bool `yaml:"NoAuthRequired"`
Authentication string `yaml:"Authentication"`
// For Outgoing Mail
POPAuth bool `yaml:"POPAuth"`
SMTPLast bool `yaml:"SMTPLast"`
// For WebMail (Unused)
UsernameDivID string `yaml:"UsernameDivID"`
UsernameDivName string `yaml:"UsernameDivName"`
PasswordDivName string `yaml:"PasswordDivName"`
SubmitButtonID string `yaml:"SubmitButtonID"`
SubmitButtonName string `yaml:"SubmitButtonName"`
}
type Response struct {
Url string `json:"url"`
ContentType string `json:"content_type"`
Message string `json:"message"`
Content map[string]interface{} `json:"content"`
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

@ -30,7 +30,7 @@
<addressBook type="{{ .Type | lower }}"> <addressBook type="{{ .Type | lower }}">
<username>{{ $.Email | parseUsername . }}</username> <username>{{ $.Email | parseUsername . }}</username>
<authentication>{{ .Authentication }}</authentication> <authentication>{{ .Authentication }}</authentication>
<serverURL>{{ .Server }}</serverURL> <serverURL>{{ .Server }}addressbooks/users/{{ $.Email | parseUsername . }}/contacts/</serverURL>
</addressBook> </addressBook>
{{ end }} {{ end }}
{{ end }} {{ end }}
@ -39,7 +39,7 @@
<calendar type="{{ .Type | lower }}"> <calendar type="{{ .Type | lower }}">
<username>{{ $.Email | parseUsername . }}</username> <username>{{ $.Email | parseUsername . }}</username>
<authentication>{{ .Authentication }}</authentication> <authentication>{{ .Authentication }}</authentication>
<serverURL>{{ .Server }}</serverURL> <serverURL>{{ .Server }}calendars/{{ $.Email | parseUsername . }}/personal/</serverURL>
</calendar> </calendar>
{{ end }} {{ end }}
{{ end }} {{ end }}

View file

@ -1,21 +1,22 @@
package handler package handler
import ( import (
. "mailautoconf/structs"
. "mailautoconf/global" . "mailautoconf/global"
. "mailautoconf/global/structs"
"mailautoconf/global/logger"
"mailautoconf/web/responses" "mailautoconf/web/responses"
"strings" "strings"
"net/http" "net/http"
"fmt" "fmt"
) )
func WebHandler(w http.ResponseWriter, r *http.Request) { func WebHandler(w http.ResponseWriter, r *http.Request) {
ThisSession = Session{} ThisSession = Session{}
ThisSession.ResponseWriter = w ThisSession.ResponseWriter = w
ThisSession.Request = r ThisSession.Request = r
ThisSession.ID = NewSessionID() ThisSession.ID = NewSessionID()
fmt.Printf("Session %s Request For : %s\r\f",ThisSession.ID, r.URL) url := fmt.Sprintf("%s", r.URL)
logger.Log("Session ", ThisSession.ID, " Request For : ", url )
ThisSession.IP = GetSessionIP() ThisSession.IP = GetSessionIP()
ThisSession.Path = strings.ToLower(r.URL.Path[1:]) ThisSession.Path = strings.ToLower(r.URL.Path[1:])
@ -35,11 +36,9 @@ func WebHandler(w http.ResponseWriter, r *http.Request) {
default: default:
ThisSession.WebContent = responses.DefaultResponse() ThisSession.WebContent = responses.DefaultResponse()
} }
webOutput()
writeWebOutput()
} }
func webOutput(){
func writeWebOutput () {
ThisSession.ResponseWriter.Header().Set("Content-Type", ThisSession.ContentType) ThisSession.ResponseWriter.Header().Set("Content-Type", ThisSession.ContentType)
fmt.Fprintf(ThisSession.ResponseWriter, ThisSession.WebContent) fmt.Fprintf(ThisSession.ResponseWriter, ThisSession.WebContent)
} }

View file

@ -1,7 +1,8 @@
package responses package responses
import ( import (
"mailautoconf/global" "mailautoconf/global"
. "mailautoconf/structs" "mailautoconf/global/logger"
. "mailautoconf/global/structs"
// "text/template" // "text/template"
"fmt" "fmt"
// "path" // "path"
@ -16,9 +17,7 @@ func MozAutoconfig() string {
// https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat // https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
// parse the querystring // parse the querystring
if err := global.ThisSession.Request.ParseForm(); err != nil { logger.CheckError(global.ThisSession.Request.ParseForm())
fmt.Println(err)
}
// build the response // build the response
response := Response{} response := Response{}
@ -33,9 +32,7 @@ func MozAutoconfig() string {
var result bytes.Buffer var result bytes.Buffer
template := global.Templates["autoconfig.xml"] template := global.Templates["autoconfig.xml"]
err := template.Execute(&result, response) err := template.Execute(&result, response)
if err != nil { logger.CheckError(err)
fmt.Println(err)
}
// return our string of xml // return our string of xml
return result.String() return result.String()
@ -53,9 +50,7 @@ func MsAutoDiscoverXML() string {
// </Autodiscover> // </Autodiscover>
// Parse the form to get the values // Parse the form to get the values
if err := global.ThisSession.Request.ParseForm(); err != nil { logger.CheckError(global.ThisSession.Request.ParseForm())
fmt.Println(err)
}
// convert the input to a string so we can extract the email address // convert the input to a string so we can extract the email address
form := fmt.Sprintf("%s",global.ThisSession.Request.Form) form := fmt.Sprintf("%s",global.ThisSession.Request.Form)
@ -68,7 +63,7 @@ func MsAutoDiscoverXML() string {
replace := regexp.MustCompile(`\<[\/]?EMailAddress\>`) replace := regexp.MustCompile(`\<[\/]?EMailAddress\>`)
email = replace.ReplaceAllString(email,``) email = replace.ReplaceAllString(email,``)
fmt.Printf("Session %s Request for email : %s\r\f",global.ThisSession.ID,email) logger.Log("Session ",global.ThisSession.ID ," Request for email : ",email)
// build the reponse // build the reponse
response := Response{} response := Response{}
response.Email = email response.Email = email
@ -79,9 +74,7 @@ func MsAutoDiscoverXML() string {
global.ThisSession.ContentType = "application/xml" global.ThisSession.ContentType = "application/xml"
var result bytes.Buffer var result bytes.Buffer
err := template.Execute(&result, response) err := template.Execute(&result, response)
if err != nil { logger.CheckError(err)
fmt.Println(err)
}
// return our string of xml // return our string of xml
return result.String() return result.String()
@ -93,7 +86,6 @@ func MsAutoDiscoverJSON() string {
// /autodiscover/autodiscover.json?Email=you@your.domain&Protocol=Autodiscoverv1&RedirectCount=1 // /autodiscover/autodiscover.json?Email=you@your.domain&Protocol=Autodiscoverv1&RedirectCount=1
email = global.ThisSession.Request.FormValue("Email") email = global.ThisSession.Request.FormValue("Email")
protocol := global.ThisSession.Request.FormValue("Protocol") protocol := global.ThisSession.Request.FormValue("Protocol")
fmt.Println(protocol)
global.ThisSession.ContentType = "application/json" global.ThisSession.ContentType = "application/json"
switch strings.ToLower(protocol) { switch strings.ToLower(protocol) {
case "autodiscoverv1": case "autodiscoverv1":
@ -102,13 +94,11 @@ func MsAutoDiscoverJSON() string {
response.Url = fmt.Sprintf("%s/Autodiscover/Autodiscover.xml", global.MainConfig.BaseURL) response.Url = fmt.Sprintf("%s/Autodiscover/Autodiscover.xml", global.MainConfig.BaseURL)
return global.JSONify(response) return global.JSONify(response)
default: default:
response := MSAutodiscoverJSONError{} response := MSAutodiscoverJSONError{}
response.ErrorCode = "InvalidProtocol"; response.ErrorCode = "InvalidProtocol";
response.ErrorMessage = fmt.Sprintf("The given protocol value '%s' is invalid. Supported values are 'AutodiscoverV1'", protocol) response.ErrorMessage = fmt.Sprintf("The given protocol value '%s' is invalid. Supported values are 'AutodiscoverV1'", protocol)
return global.JSONify(response) return global.JSONify(response)
} }
} }
func DefaultResponse() string { func DefaultResponse() string {
response := Response{} response := Response{}
@ -120,6 +110,6 @@ func DefaultResponse() string {
} }
func OurConfig() string { func OurConfig() string {
global.ThisSession.ContentType = "application/json" global.ThisSession.ContentType = "application/json"
content := global.JSONify(global.MainConfig) content := global.JSONifyConfig(global.MainConfig)
return content return content
} }