diff --git a/addy.go b/addy.go
new file mode 100644
index 0000000..fcb76d4
--- /dev/null
+++ b/addy.go
@@ -0,0 +1,164 @@
+package main
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base32"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "math/rand"
+ "net/http"
+ "os"
+ "strings"
+)
+
+var apiSecret string
+
+type addyForm struct {
+ Alias string `json:"alias"`
+ Description string `json:"description"`
+ Domain string `json:"domain"`
+}
+
+type addyResponse struct {
+ Data addyResponseData `json:"data"`
+}
+type addyResponseData struct {
+ Email string `json:"email"`
+}
+
+func init() {
+ if val, ok := os.LookupEnv("ADDY_API_SECRET"); ok {
+ apiSecret = val
+ }
+
+ flag.StringVar(&apiSecret, "addy-api-secret", apiSecret, "Secret used for the HMAC protecting addy.io-like API")
+}
+
+func AddyAPISignature(username string) []byte {
+ mac := hmac.New(sha256.New224, []byte(apiSecret))
+ mac.Write([]byte(username))
+ return mac.Sum(nil)
+}
+
+func AddyAPIToken(username string) string {
+ chain := []byte(username + ":")
+ chain = append(chain, AddyAPISignature(username)...)
+ return base32.StdEncoding.EncodeToString(chain)
+}
+
+func checkAddyApiAuthorization(authorization []byte) *string {
+ fields := bytes.SplitN(authorization, []byte(":"), 2)
+ if len(fields) != 2 {
+ return nil
+ }
+
+ username := string(fields[0])
+
+ expectedSign := AddyAPISignature(username)
+ if !hmac.Equal(expectedSign, fields[1]) {
+ return nil
+ }
+
+ return &username
+}
+
+func addyAliasAPI(w http.ResponseWriter, r *http.Request) {
+ // Check authorization header
+ fields := strings.Fields(r.Header.Get("Authorization"))
+ if len(fields) != 2 || fields[0] != "Bearer" {
+ http.Error(w, "Authorization header should be a valid Bearer token", http.StatusUnauthorized)
+ return
+ }
+
+ // Decode header
+ authorization, err := base32.StdEncoding.DecodeString(fields[1])
+ if err != nil {
+ log.Println("Invalid Authorization header: %s", err.Error())
+ http.Error(w, "Authorization header should be a valid Bearer token", http.StatusUnauthorized)
+ return
+ }
+
+ user := checkAddyApiAuthorization(authorization)
+ if user == nil {
+ http.Error(w, "Not authorized", http.StatusUnauthorized)
+ return
+ }
+
+ // Decode body
+ var body addyForm
+ err = json.NewDecoder(r.Body).Decode(&body)
+ if err != nil {
+ http.Error(w, "Invalid body", http.StatusBadRequest)
+ return
+ }
+
+ conn, err := myLDAP.Connect()
+ if err != nil || conn == nil {
+ log.Println(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ err = conn.ServiceBind()
+ if err != nil {
+ log.Println(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ dn, err := conn.SearchDN(*user, true)
+ if err != nil {
+ log.Println(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ if len(body.Alias) == 0 {
+ body.Alias = generateRandomString(10)
+ }
+
+ email := body.Alias + "@" + body.Domain
+ nb, err := conn.SearchMailAlias(email)
+ if err != nil {
+ log.Println(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if nb != 0 {
+ http.Error(w, fmt.Sprintf("The alias %q is already used.", email), http.StatusBadRequest)
+ return
+ }
+
+ err = conn.AddMailAlias(dn, email)
+ if err != nil {
+ log.Println(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ log.Printf("New alias created for %s: %s", dn, email)
+
+ res := addyResponse{
+ Data: addyResponseData{
+ Email: email,
+ },
+ }
+ err = json.NewEncoder(w).Encode(res)
+ if err != nil {
+ log.Println(err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
+func generateRandomString(length int) string {
+ charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ result := make([]byte, length)
+ for i := range result {
+ result[i] = charset[rand.Intn(len(charset))]
+ }
+ return string(result)
+}
diff --git a/ldap.go b/ldap.go
index 5a3e1a6..773d1a9 100644
--- a/ldap.go
+++ b/ldap.go
@@ -135,3 +135,34 @@ func (l LDAPConn) ChangePassword(dn string, rawpassword string) error {
return l.connection.Modify(modify)
}
+
+func (l LDAPConn) AddMailAlias(dn string, alias string) error {
+ modify := ldap.NewModifyRequest(dn, nil)
+ modify.Add("mailAlias", []string{alias})
+
+ return l.connection.Modify(modify)
+}
+
+func (l LDAPConn) SearchMailAlias(address string) (int, error) {
+ searchRequest := ldap.NewSearchRequest(
+ l.BaseDN,
+ ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+ fmt.Sprintf("(&(objectClass=*)(mailAlias=%s))", address),
+ []string{"dn"},
+ nil,
+ )
+
+ sr, err := l.connection.Search(searchRequest)
+ if err != nil {
+ return -1, err
+ }
+
+ return len(sr.Entries), nil
+}
+
+func (l LDAPConn) DelMailAlias(dn string, alias string) error {
+ modify := ldap.NewModifyRequest(dn, nil)
+ modify.Delete("mailAlias", []string{alias})
+
+ return l.connection.Modify(modify)
+}
diff --git a/login.go b/login.go
index af91fb1..a702e07 100644
--- a/login.go
+++ b/login.go
@@ -60,7 +60,7 @@ func tryLogin(w http.ResponseWriter, r *http.Request) {
}
}
}
- displayTmpl(w, "message.html", map[string]interface{}{"details": template.HTML(`Login ok
Here are the information we have about you:` + cnt + "")})
+ displayTmpl(w, "message.html", map[string]interface{}{"details": template.HTML(`Login ok
Here are the information we have about you:` + cnt + "
To use our Addy.io compatible API, use the following token: " + AddyAPIToken(r.PostFormValue("login")) + "