initial POC
This commit is contained in:
62
internal/app/app.go
Normal file
62
internal/app/app.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"text/template"
|
||||
|
||||
"git.asperti.com/paspo/mail-autoconfig/internal/config"
|
||||
"git.asperti.com/paspo/mail-autoconfig/internal/db"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var Domains map[string]Domain
|
||||
var Router *gin.Engine
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
Domains = make(map[string]Domain)
|
||||
|
||||
thunderbirdTemplate, err = template.New("thunderbird").Parse(thunderbirdTpl)
|
||||
if err != nil {
|
||||
log.Fatal(err) // TODO
|
||||
}
|
||||
|
||||
outlookTemplate, err = template.New("outlook").Parse(outlookTpl)
|
||||
if err != nil {
|
||||
log.Fatal(err) // TODO
|
||||
}
|
||||
|
||||
Router = gin.Default()
|
||||
Router.GET("/.well-known/autoconfig/mail/config-v1.1.xml", RenderThunderbird)
|
||||
Router.GET("/mail/config-v1.1.xml", RenderThunderbird)
|
||||
Router.POST("/autodiscover/autodiscover.xml", RenderOutlook)
|
||||
}
|
||||
|
||||
func FetchAllDomains() error {
|
||||
var err error
|
||||
rows, err := db.DB.Queryx("select * from domains")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var d Domain
|
||||
err := rows.StructScan(&d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
Domains[d.Domain] = d
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Start() {
|
||||
address := fmt.Sprintf(":%d", config.AppConfig.HttpPort)
|
||||
Router.Run(address)
|
||||
}
|
||||
26
internal/app/domain.go
Normal file
26
internal/app/domain.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package app
|
||||
|
||||
type Domain struct {
|
||||
Domain string `db:"domain"`
|
||||
DisplayName string `db:"display_name"`
|
||||
DisplayShortName string `db:"display_shortname"`
|
||||
IMAPEnabled bool `db:"imap_enabled"`
|
||||
IMAPServer string `db:"imap_server"`
|
||||
IMAPPort int `db:"imap_port"`
|
||||
IMAPSSL bool `db:"imap_ssl"`
|
||||
IMAPSPA bool `db:"imap_spa"`
|
||||
POP3Enabled bool `db:"pop3_enabled"`
|
||||
POP3Server string `db:"pop3_server"`
|
||||
POP3Port int `db:"pop3_port"`
|
||||
POP3SSL bool `db:"pop3_ssl"`
|
||||
POP3SPA bool `db:"pop3_spa"`
|
||||
SMTPEnabled bool `db:"smtp_enabled"`
|
||||
SMTPServer string `db:"smtp_server"`
|
||||
SMTPPort int `db:"smtp_port"`
|
||||
SMTPSSL bool `db:"smtp_ssl"`
|
||||
SMTPTLS bool `db:"smtp_tls"`
|
||||
SMTPSPA bool `db:"smtp_spa"`
|
||||
POPBeforeSMTP bool `db:"pop_before_smtp"`
|
||||
DomainRequired bool `db:"domain_required"`
|
||||
Username string
|
||||
}
|
||||
135
internal/app/outlook.go
Normal file
135
internal/app/outlook.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const outlookTpl = `
|
||||
<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>
|
||||
|
||||
{{- if .IMAPEnabled }}
|
||||
<Protocol>
|
||||
<Type>IMAP</Type>
|
||||
<Server>{{ .IMAPServer }}</Server>
|
||||
<Port>{{ .IMAPPort }}</Port>
|
||||
<DomainRequired>on</DomainRequired>
|
||||
<SPA>off</SPA>
|
||||
<SSL>{{ if .IMAPSSL }}on{{ else }}off{{ end }}</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
<LoginName>{{ .Username }}@{{ .Domain }}</LoginName>
|
||||
</Protocol>
|
||||
{{- end }}
|
||||
|
||||
{{- if .POP3Enabled }}
|
||||
<Protocol>
|
||||
<Type>POP3</Type>
|
||||
<Server>mail.mdfmultimedia.com</Server>
|
||||
<Port>995</Port>
|
||||
<DomainRequired>on</DomainRequired>
|
||||
<SPA>off</SPA>
|
||||
<SSL>{{ if .POP3SSL }}on{{ else }}off{{ end }}</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
<LoginName>{{ .Username }}@{{ .Domain }}</LoginName>
|
||||
</Protocol>
|
||||
{{- end }}
|
||||
|
||||
{{- if .SMTPEnabled }}
|
||||
<Protocol>
|
||||
<Type>SMTP</Type>
|
||||
<Server>mail.mdfmultimedia.com</Server>
|
||||
<Port>465</Port>
|
||||
<DomainRequired>on</DomainRequired>
|
||||
<SPA>off</SPA>
|
||||
<SSL>{{ if .SMTPSSL }}on{{ else }}off{{ end }}</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
<LoginName>{{ .Username }}@{{ .Domain }}</LoginName>
|
||||
</Protocol>
|
||||
{{- end }}
|
||||
|
||||
</Account>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
`
|
||||
|
||||
var outlookTemplate *template.Template
|
||||
|
||||
func RenderOutlook(g *gin.Context) {
|
||||
var err error
|
||||
var body []byte
|
||||
var domain Domain
|
||||
var ok bool
|
||||
var b bytes.Buffer
|
||||
|
||||
body, err = io.ReadAll(g.Request.Body)
|
||||
if err != nil {
|
||||
g.JSON(404, gin.H{"code": "WRONG_BODY", "message": "malformed request"})
|
||||
return
|
||||
}
|
||||
|
||||
var e struct {
|
||||
Address string `xml:"Request>EMailAddress"`
|
||||
}
|
||||
|
||||
err = xml.Unmarshal(body, &e)
|
||||
if err != nil {
|
||||
g.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
|
||||
return
|
||||
}
|
||||
|
||||
components := strings.Split(e.Address, "@")
|
||||
username, reqDomain := components[0], components[1]
|
||||
|
||||
domain, ok = Domains[reqDomain]
|
||||
if !ok {
|
||||
g.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
|
||||
return
|
||||
}
|
||||
|
||||
domain.Username = username
|
||||
err = outlookTemplate.Execute(&b, domain)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
g.Header("Content-Type", "application/xml; charset=utf-8")
|
||||
g.String(http.StatusOK, b.String())
|
||||
}
|
||||
|
||||
// curl -X POST -d @req.xml http://127.0.0.1:8888/autodiscover/autodiscover.xml
|
||||
// curl --basic -X "POST" -u XXX@YYY -v https://autodiscover-s.outlook.com/autodiscover/autodiscover.xml -H "Content-Type: text/xml" -d @a.txt
|
||||
|
||||
/*
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006">
|
||||
<Request>
|
||||
<EMailAddress>testuser@company.tld</EMailAddress>
|
||||
<AcceptableResponseSchema>
|
||||
http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006
|
||||
</AcceptableResponseSchema>
|
||||
</Request>
|
||||
</Autodiscover>
|
||||
|
||||
$ curl -d @request.xml -u testuser@company.tld -H "Content-Type: text/xml" -v https://autodiscover.company.tld/autodiscover/autodiscover.xml
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
1 - Lookup di un record A (o CNAME) per contoso.com che punta ad un web server che risponderà con un URL HTTPS https://contoso.com/Autodiscover/Autodiscover.xml.
|
||||
2 - Lookup di un record A (o CNAME) per autodiscover.contoso.com che punta ad un web server che risponderà con un URL HTTPS https://autodiscover.contoso.com/Autodiscover/Autodiscover.xml
|
||||
3 - Lookup di un record A (o CNAME) per contoso.com che punta ad un web server che risponderà con URL HTTP http://autodiscover.contoso.com/Autodiscover/Autodiscover.xml (se avete un reverse proxy o direttamente su Exchange, dovrete fare la configurazione necessaria per implementare l’http to https redirect)
|
||||
4 - Lookup di un record SRV per autodiscover._tcp.contoso.com (Questo record deve contenere la porta 443 e l’hostname, ad esempio mail.contoso.com, permetendo cosi al client di fare una request in https all’URL https://mail.contoso.com/Autodiscover/Autodiscover.xml)
|
||||
|
||||
*/
|
||||
90
internal/app/thunderbird.go
Normal file
90
internal/app/thunderbird.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const thunderbirdTpl = `
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="{{ .Domain }}">
|
||||
<domain>{{ .Domain }}</domain>
|
||||
<displayName>{{ .DisplayName }}</displayName>
|
||||
<displayShortName>{{ .DisplayShortName }}</displayShortName>
|
||||
|
||||
{{- if .IMAPEnabled }}
|
||||
<incomingServer type="imap">
|
||||
<hostname>{{ .IMAPServer }}</hostname>
|
||||
<port>{{ .IMAPPort }}</port>
|
||||
{{- if .IMAPSSL }}
|
||||
<socketType>SSL</socketType>
|
||||
{{- else }}
|
||||
<socketType>plain</socketType>
|
||||
{{- end }}
|
||||
<authentication>password-encrypted</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
{{- end }}
|
||||
|
||||
{{- if .POP3Enabled }}
|
||||
<incomingServer type="pop3">
|
||||
<hostname>{{ .POP3Server }}</hostname>
|
||||
<port>{{ .POP3Port }}</port>
|
||||
{{- if .POP3SSL }}
|
||||
<socketType>SSL</socketType>
|
||||
{{- else }}
|
||||
<socketType>plain</socketType>
|
||||
{{- end }}
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
{{- end }}
|
||||
|
||||
{{- if .SMTPEnabled }}
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>{{ .SMTPServer }}</hostname>
|
||||
<port>{{ .SMTPPort }}</port>
|
||||
{{- if .SMTPSSL }}
|
||||
<socketType>SSL</socketType>
|
||||
{{- else }}
|
||||
{{if .SMTPTLS }}
|
||||
<socketType>STARTTLS</socketType>
|
||||
{{- else }}
|
||||
<socketType>plain</socketType>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
<authentication>password-encrypted</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</outgoingServer>
|
||||
{{- end }}
|
||||
|
||||
</emailProvider>
|
||||
</clientConfig>
|
||||
`
|
||||
|
||||
var thunderbirdTemplate *template.Template
|
||||
|
||||
// https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
|
||||
|
||||
func RenderThunderbird(g *gin.Context) {
|
||||
host := g.Request.Host
|
||||
domain, ok := Domains[host]
|
||||
if !ok {
|
||||
g.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := thunderbirdTemplate.Execute(&b, domain); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
g.Header("Content-Type", "application/xml; charset=utf-8")
|
||||
g.String(http.StatusOK, b.String())
|
||||
}
|
||||
|
||||
// curl http://127.0.0.1:8888/.well-known/autoconfig/mail/config-v1.1.xml
|
||||
35
internal/config/config.go
Normal file
35
internal/config/config.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"git.asperti.com/paspo/mail-autoconfig/internal/myerrors"
|
||||
"github.com/gookit/validate"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
HttpPort int `mapstructure:"PORT" validate:"required|int|min:1|max:65535"`
|
||||
DBPath string `mapstructure:"DB_PATH" validate:"required"`
|
||||
}
|
||||
|
||||
var AppConfig Config
|
||||
|
||||
func InitConfig() error {
|
||||
var err error
|
||||
viper.SetConfigType("env")
|
||||
viper.AllowEmptyEnv(true)
|
||||
viper.AutomaticEnv()
|
||||
|
||||
viper.SetDefault("PORT", 9000)
|
||||
viper.SetDefault("DB_PATH", "./database.sqlite")
|
||||
|
||||
err = viper.Unmarshal(&AppConfig)
|
||||
if err != nil {
|
||||
return myerrors.ErrConfigParse
|
||||
}
|
||||
|
||||
validation := validate.Struct(AppConfig)
|
||||
if !validation.Validate() {
|
||||
return myerrors.ErrConfigValidation
|
||||
}
|
||||
return nil
|
||||
}
|
||||
155
internal/db/db.go
Normal file
155
internal/db/db.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.asperti.com/paspo/mail-autoconfig/internal/config"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var (
|
||||
dbStructure []string
|
||||
)
|
||||
|
||||
var DB *sqlx.DB
|
||||
|
||||
func init() {
|
||||
dbStructure = []string{
|
||||
`CREATE TABLE IF NOT EXISTS domains (
|
||||
domain VARCHAR PRIMARY KEY,
|
||||
display_name VARCHAR,
|
||||
display_shortname VARCHAR,
|
||||
imap_enabled BOOL,
|
||||
imap_server VARCHAR,
|
||||
imap_port INT,
|
||||
imap_ssl BOOL,
|
||||
imap_spa BOOL,
|
||||
pop3_enabled BOOL,
|
||||
pop3_server VARCHAR,
|
||||
pop3_port INT,
|
||||
pop3_ssl BOOL,
|
||||
pop3_spa BOOL,
|
||||
smtp_enabled BOOL,
|
||||
smtp_server VARCHAR,
|
||||
smtp_port INT,
|
||||
smtp_ssl BOOL,
|
||||
smtp_tls BOOL,
|
||||
smtp_spa BOOL,
|
||||
pop_before_smtp BOOL,
|
||||
domain_required BOOL);`,
|
||||
}
|
||||
}
|
||||
|
||||
func InitDB() error {
|
||||
var err error
|
||||
|
||||
DB, err = sqlx.Open("sqlite3", config.AppConfig.DBPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, stmt := range dbStructure {
|
||||
_, err = DB.Exec(stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DoiooooDELME() {
|
||||
var err error
|
||||
sqlStmt := `
|
||||
create table foo (id integer not null primary key, name text);
|
||||
delete from foo;
|
||||
`
|
||||
_, err = DB.Exec(sqlStmt)
|
||||
if err != nil {
|
||||
log.Printf("%q: %s\n", err, sqlStmt)
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := DB.Begin()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
stmt, err := tx.Prepare("insert into foo(id, name) values(?, ?)")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
for i := 0; i < 100; i++ {
|
||||
_, err = stmt.Exec(i, fmt.Sprintf("こんにちは世界%03d", i))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
rows, err := DB.Query("select id, name from foo")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var name string
|
||||
err = rows.Scan(&id, &name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(id, name)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stmt, err = DB.Prepare("select name from foo where id = ?")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
var name string
|
||||
err = stmt.QueryRow("3").Scan(&name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(name)
|
||||
|
||||
_, err = DB.Exec("delete from foo")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = DB.Exec("insert into foo(id, name) values(1, 'foo'), (2, 'bar'), (3, 'baz')")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
rows, err = DB.Query("select id, name from foo")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var name string
|
||||
err = rows.Scan(&id, &name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(id, name)
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
9
internal/myerrors/myerrors.go
Normal file
9
internal/myerrors/myerrors.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package myerrors
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrConfigLoad = errors.New("Failed to load configuration")
|
||||
ErrConfigParse = errors.New("Failed to parse configuration")
|
||||
ErrConfigValidation = errors.New("Failed to validate configuration")
|
||||
)
|
||||
Reference in New Issue
Block a user