Compare commits

...

11 commits

10 changed files with 196 additions and 134 deletions

View file

@ -1,8 +1,12 @@
# 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
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?
MailAutoConf is autodiscover/autoconfig web server for self-hosted mail services
which do not have their own autodiscover service.
@ -60,7 +64,7 @@ SRV _autodiscover._tcp.your.domain 3600 10 10 443 autoconfig.your.domain
```
## 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] Evolution Mail (v3.40.3 and probably earlier versions too)
- [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?
Well, not yet. Though it does sort of work already.
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.
It works for non-Microsoft email clients now (see Compatibility above).
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.

View file

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

View file

@ -1,10 +1,11 @@
package global
import (
. "mailautoconf/structs"
. "mailautoconf/global/structs"
"mailautoconf/global/logger"
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
// "os"
"encoding/json"
"text/template"
"path"
@ -39,16 +40,16 @@ func NewConfig() Config {
}
func loadConfig() Config {
cfg := Config{}
fmt.Println("Loading Default Config…")
logger.Log("Loading Default Config…")
cfgfile := defaultConfigDir + "config.default.yaml"
unmarshalConfig(cfgfile, &cfg)
fmt.Println("Loading Custom Config…")
logger.Log("Loading Custom Config…")
customcfgfile := configDir + "config.yaml"
unmarshalConfig(customcfgfile, &cfg)
fmt.Println("Loading Default Services…")
logger.Log("Loading Default Services…")
svcfile := defaultConfigDir + "services.default.yaml"
unmarshalConfig(svcfile, &cfg)
fmt.Println("Loading Custom Services…")
logger.Log("Loading Custom Services…")
customsvcfile := configDir + "services.yaml"
unmarshalConfig(customsvcfile, &cfg)
removeDisabledItems(&cfg)
@ -64,21 +65,19 @@ func loadXMLTemplates(){
"onoff": chooseOnOff,
}
t, err := template.New(name).Funcs(fmap).ParseFiles(tmpl)
if err != nil {
panic (err)
}
logger.CheckError(err)
Templates[name] = t
}
}
func unmarshalConfig(file string, cfg *Config) {
if FileExists(file) {
if logger.FileExists(file) {
content, err := ioutil.ReadFile(file)
if err != nil {
fmt.Println("Error reading config :", file, " : ", err)
if !logger.ErrorOK(err){
logger.Log("Error reading config :", file, " : ", fmt.Sprintf("%v",err))
}
err2 := yaml.Unmarshal(content, &cfg)
if err2 != nil {
fmt.Println("Error unmarshaling config :", file, " : ", err2)
if !logger.ErrorOK(err2){
logger.Log("Error unmarshaling config :", file, " : ", fmt.Sprintf("%v",err2))
}
}
}
@ -108,22 +107,15 @@ func removeDisabledItems(cfg *Config) {
}
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 {
data, err := json.Marshal(content)
if err != nil {
fmt.Println(err)
}
logger.CheckError(err)
return string(data)
}
func parseUsername(svc Service, email string) string {
@ -160,6 +152,6 @@ func GetSessionIP() string {
if 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
}

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
import (
"fmt"
// "fmt"
"net/http"
"log"
// "log"
"mailautoconf/web/handler"
"mailautoconf/global"
"mailautoconf/global/logger"
)
func main() {
global.NewConfig()
http.HandleFunc("/", handler.WebHandler)
fmt.Println("Starting up Web Listener on port 8010")
log.Fatal(http.ListenAndServe(":8010", nil))
logger.Log("Starting up Web Listener on port 8010")
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 }}">
<username>{{ $.Email | parseUsername . }}</username>
<authentication>{{ .Authentication }}</authentication>
<serverURL>{{ .Server }}</serverURL>
<serverURL>{{ .Server }}addressbooks/users/{{ $.Email | parseUsername . }}/contacts/</serverURL>
</addressBook>
{{ end }}
{{ end }}
@ -39,7 +39,7 @@
<calendar type="{{ .Type | lower }}">
<username>{{ $.Email | parseUsername . }}</username>
<authentication>{{ .Authentication }}</authentication>
<serverURL>{{ .Server }}</serverURL>
<serverURL>{{ .Server }}calendars/{{ $.Email | parseUsername . }}/personal/</serverURL>
</calendar>
{{ end }}
{{ end }}

View file

@ -1,21 +1,22 @@
package handler
import (
. "mailautoconf/structs"
. "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()
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.Path = strings.ToLower(r.URL.Path[1:])
@ -35,11 +36,9 @@ func WebHandler(w http.ResponseWriter, r *http.Request) {
default:
ThisSession.WebContent = responses.DefaultResponse()
}
writeWebOutput()
webOutput()
}
func writeWebOutput () {
func webOutput(){
ThisSession.ResponseWriter.Header().Set("Content-Type", ThisSession.ContentType)
fmt.Fprintf(ThisSession.ResponseWriter, ThisSession.WebContent)
}

View file

@ -1,7 +1,8 @@
package responses
import (
"mailautoconf/global"
. "mailautoconf/structs"
"mailautoconf/global/logger"
. "mailautoconf/global/structs"
// "text/template"
"fmt"
// "path"
@ -16,9 +17,7 @@ func MozAutoconfig() string {
// https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
// parse the querystring
if err := global.ThisSession.Request.ParseForm(); err != nil {
fmt.Println(err)
}
logger.CheckError(global.ThisSession.Request.ParseForm())
// build the response
response := Response{}
@ -33,9 +32,7 @@ func MozAutoconfig() string {
var result bytes.Buffer
template := global.Templates["autoconfig.xml"]
err := template.Execute(&result, response)
if err != nil {
fmt.Println(err)
}
logger.CheckError(err)
// return our string of xml
return result.String()
@ -53,9 +50,7 @@ func MsAutoDiscoverXML() string {
// </Autodiscover>
// Parse the form to get the values
if err := global.ThisSession.Request.ParseForm(); err != nil {
fmt.Println(err)
}
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)
@ -68,7 +63,7 @@ func MsAutoDiscoverXML() string {
replace := regexp.MustCompile(`\<[\/]?EMailAddress\>`)
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
response := Response{}
response.Email = email
@ -79,9 +74,7 @@ func MsAutoDiscoverXML() string {
global.ThisSession.ContentType = "application/xml"
var result bytes.Buffer
err := template.Execute(&result, response)
if err != nil {
fmt.Println(err)
}
logger.CheckError(err)
// return our string of xml
return result.String()
@ -93,7 +86,6 @@ func MsAutoDiscoverJSON() string {
// /autodiscover/autodiscover.json?Email=you@your.domain&Protocol=Autodiscoverv1&RedirectCount=1
email = global.ThisSession.Request.FormValue("Email")
protocol := global.ThisSession.Request.FormValue("Protocol")
fmt.Println(protocol)
global.ThisSession.ContentType = "application/json"
switch strings.ToLower(protocol) {
case "autodiscoverv1":
@ -102,13 +94,11 @@ func MsAutoDiscoverJSON() string {
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{}
@ -120,6 +110,6 @@ func DefaultResponse() string {
}
func OurConfig() string {
global.ThisSession.ContentType = "application/json"
content := global.JSONify(global.MainConfig)
content := global.JSONifyConfig(global.MainConfig)
return content
}