diff --git a/.gitignore b/.gitignore index 04204c7..77bb3a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ config +src/config diff --git a/Containerfile b/Containerfile index f8c164e..f7f2701 100644 --- a/Containerfile +++ b/Containerfile @@ -1,15 +1,22 @@ -FROM php:7.4-apache +FROM golang:1-alpine3.14 AS builder -COPY src/ /var/www/html/ +COPY src/ /mailautoconf +WORKDIR /mailautoconf +RUN go build -o /mailautoconf/mailautoconf + +FROM alpine:3.14 + +ENV UID=1426 \ + GID=1426 + +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 / RUN chmod +x /entrypoint.sh -# Use the default production configuration -RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" - -RUN a2enmod rewrite - -EXPOSE 80 +EXPOSE 8010 ENTRYPOINT ["/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh index 57b8053..b3613b1 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,29 +1,29 @@ #!/usr/bin/env bash echo Removing old sample files… -rm /var/www/html/config/*.sample.ini +rm /mailautoconf/config/*.sample.yaml function write_file() { while read line; do first_char=${line:0:1} - if [[ $first_char != ";" ]]; then - line="; "$line + if [[ $first_char != "#" ]]; then + line="#"$line fi echo $line >> $2 done < $1 } echo Setting up new sample config files… -def_conf="/var/www/html/default-config/config.default.ini" -new_conf="/var/www/html/config/config.sample.ini" +def_conf="/mailautoconf/default-config/config.default.yaml" +new_conf="/mailautoconf/config/config.sample.yaml" write_file $def_conf $new_conf -def_serv="/var/www/html/default-config/services.default.ini" -new_serv="/var/www/html/config/services.sample.ini" +def_serv="/mailautoconf/default-config/services.default.yaml" +new_serv="/mailautoconf/config/services.sample.yaml" write_file $def_serv $new_serv +echo New sample files copied - -echo Running HTTPD… -exec apache2-foreground +cd /mailautoconf +exec /mailautoconf/mailautoconf diff --git a/src/default-config/config.default.yaml b/src/default-config/config.default.yaml new file mode 100644 index 0000000..870d458 --- /dev/null +++ b/src/default-config/config.default.yaml @@ -0,0 +1,17 @@ +--- +Version : "0.1.1" +# 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" diff --git a/src/default-config/services.default.yaml b/src/default-config/services.default.yaml new file mode 100644 index 0000000..52d636b --- /dev/null +++ b/src/default-config/services.default.yaml @@ -0,0 +1,133 @@ +# 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" diff --git a/src/global/global.go b/src/global/global.go new file mode 100644 index 0000000..b690295 --- /dev/null +++ b/src/global/global.go @@ -0,0 +1,70 @@ +package global +import ( + . "mailautoconf/structs" + "fmt" + "gopkg.in/yaml.v2" + "io/ioutil" + "os" + "encoding/json" + +) + +// Global variables +var ThisSession Session +var MainConfig Config +const defaultConfigDir string = "default-config/" +const configDir string = "config/" + +func NewConfig() Config { + MainConfig = loadConfig() + return MainConfig +} +func loadConfig() Config { + cfg := Config{} + fmt.Println("Loading Default Config…") + cfgfile := defaultConfigDir + "config.default.yaml" + unmarshalConfig(cfgfile, &cfg) + fmt.Println("Loading Custom Config…") + customcfgfile := configDir + "config.yaml" + unmarshalConfig(customcfgfile, &cfg) + fmt.Println("Loading Default Services…") + svcfile := defaultConfigDir + "services.default.yaml" + unmarshalConfig(svcfile, &cfg) + fmt.Println("Loading Custom Services…") + customsvcfile := configDir + "services.yaml" + unmarshalConfig(customsvcfile, &cfg) + return cfg +} + +func unmarshalConfig(file string, cfg *Config) { + if FileExists(file) { + content, err := ioutil.ReadFile(file) + if err != nil { + fmt.Println("Error reading config :", file, " : ", err) + } + err2 := yaml.Unmarshal(content, &cfg) + if err2 != nil { + fmt.Println("Error unmarshalling config :", file, " : ", err2) + } + } +} + + +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 JSONify(content interface{}) string { + data, err := json.Marshal(content) + if err != nil { + fmt.Println(err) + } + return string(data) +} diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..db9995d --- /dev/null +++ b/src/go.mod @@ -0,0 +1,15 @@ +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 +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..bd2f174 --- /dev/null +++ b/src/go.sum @@ -0,0 +1,28 @@ +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= diff --git a/src/mailautoconf.go b/src/mailautoconf.go new file mode 100644 index 0000000..a42da48 --- /dev/null +++ b/src/mailautoconf.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "net/http" + "log" + "mailautoconf/web/handler" + "mailautoconf/global" +) + +func main() { + global.NewConfig() + http.HandleFunc("/", handler.WebHandler) + fmt.Println("Starting up Web Listener on port 8010") + log.Fatal(http.ListenAndServe(":8010", nil)) +} diff --git a/src/structs/structs.go b/src/structs/structs.go new file mode 100644 index 0000000..358a0da --- /dev/null +++ b/src/structs/structs.go @@ -0,0 +1,64 @@ +package structs + +import "net/http" + + +type Session struct { + 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 + Email string +} +type MSAutodiscoverJSONResponse struct { + // More work to do - handling of MS Autodiscover.json requests + Protocol string + Url string +} +type MSAutodiscoverJSONError struct{ + ErrorCode string + ErrorMessage string +} diff --git a/src/templates/autoconfig.xml b/src/templates/autoconfig.xml new file mode 100644 index 0000000..9320e50 --- /dev/null +++ b/src/templates/autoconfig.xml @@ -0,0 +1,50 @@ + + + {{ range .Config.Domains }}{{ . }} + {{ end }} + {{ .Email }} + {{ with .Config.InMail }} + + {{ .Server }} + {{ .Port }} + {{ .SocketType }} + {{ . | parseUsername }} + {{ .Authentication }} + + {{ end }} + {{ with .Config.OutMail }} + + {{ .Server }} + {{ .Port }}> + {{ .SocketType }} + {{ . | parseUsername }} + {{ .Authentication }} + + {{ end }} + {{ with .Config.AddressBook }} + + {{ . | parseUsername }} + {{ .Authentication }} + {{ .Server }} + + {{ end }} + {{ with .Config.Calendar }} + + {{ . | parseUsername }} + {{ .Authentication }} + {{ .Server }} + + {{ end }} + {{ with .Config.WebMail }} + + + + {{ . | parseUsername }} + + + + + + {{ end }} + + diff --git a/src/templates/autodiscover.xml b/src/templates/autodiscover.xml new file mode 100644 index 0000000..b024b74 --- /dev/null +++ b/src/templates/autodiscover.xml @@ -0,0 +1,35 @@ + + + + + email + settings + {{ with .Config.InMail }} + + {{ .Type }} + {{ .Server }} + {{ .Port }} + {{ .UsernameIsFQDN | onoff }} + {{ . | parseUsername }} + {{ .SPA | onoff }} + {{ if eq .SocketType "SSL" }}on{{ else }}off{{ end }} + {{ not .NoAuthRequired | onoff }} + + {{ end }} + {{ with .Config.OutMail }} + + {{ .Type }} + {{ .Server }} + {{ .Port }} + {{ .UsernameIsFQDN | onoff }} + {{ . | parseUsername }} + {{ .SPA | onoff }} + {{ .SocketType }} + {{ not .NoAuthRequired | onoff }} + {{ .POPAuth | onoff }} + {{ .SMTPLast | onoff }} + + {{ end }} + + + diff --git a/src/web/handler/handler.go b/src/web/handler/handler.go new file mode 100644 index 0000000..f9ceb1f --- /dev/null +++ b/src/web/handler/handler.go @@ -0,0 +1,39 @@ +package handler +import ( + . "mailautoconf/structs" + . "mailautoconf/global" + "mailautoconf/web/responses" + "strings" + "net/http" + "fmt" +) +func WebHandler(w http.ResponseWriter, r *http.Request) { + fmt.Println("Request For :",r.URL) + ThisSession = Session{} + ThisSession.ResponseWriter = w + ThisSession.Request = r + 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() + } + + writeWebOutput() +} + +func writeWebOutput () { + ThisSession.ResponseWriter.Header().Set("Content-Type", ThisSession.ContentType) + fmt.Fprintf(ThisSession.ResponseWriter, ThisSession.WebContent) +} diff --git a/src/web/responses/responses.go b/src/web/responses/responses.go new file mode 100644 index 0000000..d0cc973 --- /dev/null +++ b/src/web/responses/responses.go @@ -0,0 +1,105 @@ +package responses +import ( + "mailautoconf/global" + . "mailautoconf/structs" + "text/template" + "fmt" + "path" + "strings" + "bytes" + "regexp" +) +var email string +var fmap = template.FuncMap{ + "lower": strings.ToLower, + "parseUsername": parseUsername, + "onoff": chooseOnOff, + } +func MozAutoconfig() string { + // The below link has config-v1.1.xml information + // https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat + tmpl := "templates/autoconfig.xml" + response := Response{} + response.Email = global.ThisSession.Request.FormValue("emailaddress") + response.Config = global.MainConfig + + name := path.Base(tmpl) + t, err1 := template.New(name).Funcs(fmap).ParseFiles(tmpl) + if err1 != nil { + panic (err1) + } + global.ThisSession.ContentType = "application/xml" + var result bytes.Buffer + err := t.Execute(&result, response) + if err != nil { + fmt.Println(err) + } + return result.String() +} +func MsAutoDiscoverXML() string { + // Example POST Request (sent from client) : + // + // + // + // your@email.address + // http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a + // + // + tmpl := "templates/autodiscover.xml" + email = global.ThisSession.Request.FormValue("EMailAddress") + response := Response{} + response.Config = global.MainConfig + name := path.Base(tmpl) + t, err1 := template.New(name).Funcs(fmap).ParseFiles(tmpl) + if err1 != nil { + panic (err1) + } + global.ThisSession.ContentType = "application/xml" + var result bytes.Buffer + err := t.Execute(&result, response) + if err != nil { + fmt.Println(err) + } + return result.String() +} +func MsAutoDiscoverJSON() string { + return "" +} +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.JSONify(global.MainConfig) + return content +} +func parseUsername(svc Service) string { + if email == "" { + return "" + } + if svc.UsernameIsFQDN && !svc.RequireLocalDomain{ + return email + } else if svc.UsernameIsFQDN && svc.RequireLocalDomain { + re := regexp.MustCompile(`[^@]+$`) + domain := re.FindString(email) + localemail := strings.Replace(email, domain, + global.MainConfig.LocalDomain,1) + return localemail + } else { + re := regexp.MustCompile(`^[^@]+`) + username := re.FindString(email) + return username + } +} +func chooseOnOff(value bool) string { + if value { + return "on" + } else { + return "off" + } +}