779 lines
25 KiB
Go
779 lines
25 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto/rand"
|
||
|
"errors"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"crypto/ed25519"
|
||
|
"database/sql"
|
||
|
"encoding/json"
|
||
|
"html/template"
|
||
|
"io/fs"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
|
||
|
library "git.ailur.dev/ailur/fg-library/v2"
|
||
|
nucleusLibrary "git.ailur.dev/ailur/fg-nucleus-library"
|
||
|
"github.com/emersion/go-sasl"
|
||
|
"github.com/golang-jwt/jwt/v5"
|
||
|
"github.com/google/uuid"
|
||
|
|
||
|
_ "github.com/lib/pq"
|
||
|
)
|
||
|
|
||
|
var ServiceInformation = library.Service{
|
||
|
Name: "kittemail",
|
||
|
Permissions: library.Permissions{
|
||
|
Authenticate: true, // This service does require authentication
|
||
|
Database: true, // This service does require database access
|
||
|
BlobStorage: true, // This service does require reserving blob storage space
|
||
|
InterServiceCommunication: true, // This service does require inter-service communication
|
||
|
Resources: true, // This service does require its HTTP templates and static files
|
||
|
},
|
||
|
ServiceID: uuid.MustParse("338350fc-14d5-469d-999b-950033fb6b7c"),
|
||
|
}
|
||
|
|
||
|
func getUUIDBytes(uuidString string) []byte {
|
||
|
uuidBytes := uuid.MustParse(uuidString)
|
||
|
return uuidBytes[:]
|
||
|
}
|
||
|
|
||
|
func randomChars(length int) (string, error) {
|
||
|
var saltChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
|
if length <= 0 {
|
||
|
return "", errors.New("salt length must be greater than 0")
|
||
|
}
|
||
|
|
||
|
salt := make([]byte, length)
|
||
|
randomBytes := make([]byte, length)
|
||
|
_, err := rand.Read(randomBytes)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
for i := range salt {
|
||
|
salt[i] = saltChars[int(randomBytes[i])%len(saltChars)]
|
||
|
}
|
||
|
return string(salt), nil
|
||
|
}
|
||
|
|
||
|
func logFunc(message string, messageType uint64, information library.ServiceInitializationInformation) {
|
||
|
// Log the message to the logger service
|
||
|
information.Outbox <- library.InterServiceMessage{
|
||
|
ServiceID: ServiceInformation.ServiceID,
|
||
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000002"), // Logger service
|
||
|
MessageType: messageType,
|
||
|
SentAt: time.Now(),
|
||
|
Message: message,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func renderTemplate(statusCode int, w http.ResponseWriter, data any, templatePath string, information library.ServiceInitializationInformation) {
|
||
|
var err error
|
||
|
var requestedTemplate *template.Template
|
||
|
// Output ls of the resource directory
|
||
|
requestedTemplate, err = template.ParseFS(information.ResourceDir, "templates/"+templatePath)
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 2, information)
|
||
|
renderString(500, w, "Sorry, something went wrong on our end. Error code: 01. Please report to the administrator.", information)
|
||
|
} else {
|
||
|
w.WriteHeader(statusCode)
|
||
|
err = requestedTemplate.Execute(w, data)
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 2, information)
|
||
|
renderString(500, w, "Sorry, something went wrong on our end. Error code: 02. Please report to the administrator.", information)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func renderString(statusCode int, w http.ResponseWriter, data string, information library.ServiceInitializationInformation) {
|
||
|
w.WriteHeader(statusCode)
|
||
|
_, err := w.Write([]byte(data))
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 2, information)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func renderJSON(statusCode int, w http.ResponseWriter, data any, information library.ServiceInitializationInformation) {
|
||
|
w.WriteHeader(statusCode)
|
||
|
err := json.NewEncoder(w).Encode(data)
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 2, information)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func verifyJwt(token string, publicKey ed25519.PublicKey, conn library.Database, checkInUsers bool) (jwt.MapClaims, bool) {
|
||
|
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||
|
return publicKey, nil
|
||
|
})
|
||
|
if err != nil {
|
||
|
println(err.Error())
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
if !parsedToken.Valid {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
||
|
if !ok {
|
||
|
return nil, false
|
||
|
}
|
||
|
|
||
|
// Check if the token expired
|
||
|
date, err := claims.GetExpirationTime()
|
||
|
if err != nil || date.Before(time.Now()) || claims["sub"] == nil || claims["isOpenID"] == nil || claims["isOpenID"].(bool) {
|
||
|
return claims, false
|
||
|
}
|
||
|
|
||
|
// Check if the token is in users
|
||
|
if checkInUsers {
|
||
|
var idCheck []byte
|
||
|
err = conn.DB.QueryRow("SELECT id FROM users WHERE id = $1", getUUIDBytes(claims["sub"].(string))).Scan(&idCheck)
|
||
|
if err != nil || claims["sub"] != uuid.Must(uuid.FromBytes(idCheck)).String() {
|
||
|
return claims, false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return claims, true
|
||
|
}
|
||
|
|
||
|
func initDb(conn library.Database, information library.ServiceInitializationInformation) {
|
||
|
if conn.DBType == library.Sqlite {
|
||
|
// Create the users table
|
||
|
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BLOB isPrimary KEY, subscription INTEGER NOT NULL, createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 3, information)
|
||
|
}
|
||
|
|
||
|
// Create the emails table
|
||
|
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS emails (username TEXT NOT NULL, address TEXT NOT NULL, isPrimary INTEGER NOT NULL, owner BLOB NOT NULL, createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, password TEXT NOT NULL)")
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 3, information)
|
||
|
}
|
||
|
} else {
|
||
|
// Create the users table
|
||
|
_, err := conn.DB.Exec("CREATE TABLE IF NOT EXISTS users (id BYTEA isPrimary KEY, subscription INTEGER NOT NULL, createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP)")
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 3, information)
|
||
|
}
|
||
|
|
||
|
// Create the emails table
|
||
|
_, err = conn.DB.Exec("CREATE TABLE IF NOT EXISTS emails (username TEXT NOT NULL, address TEXT NOT NULL, isPrimary BOOLEAN NOT NULL, owner BYTEA NOT NULL, createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, password TEXT NOT NULL)")
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 3, information)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func getSuperuserCommand(information library.ServiceInitializationInformation) string {
|
||
|
if os.Getuid() != 0 {
|
||
|
// Check if pkexec, sudo or doas is installed
|
||
|
_, err := os.Stat("/usr/bin/pkexec")
|
||
|
if os.IsNotExist(err) {
|
||
|
_, err = os.Stat("/usr/bin/sudo")
|
||
|
if os.IsNotExist(err) {
|
||
|
_, err = os.Stat("/usr/bin/doas")
|
||
|
if os.IsNotExist(err) {
|
||
|
logFunc("No superuser command found", 3, information)
|
||
|
} else if err != nil {
|
||
|
logFunc(err.Error(), 3, information)
|
||
|
} else {
|
||
|
return "doas"
|
||
|
}
|
||
|
} else if err != nil {
|
||
|
logFunc(err.Error(), 3, information)
|
||
|
} else {
|
||
|
return "sudo"
|
||
|
}
|
||
|
} else if err != nil {
|
||
|
logFunc(err.Error(), 3, information)
|
||
|
} else {
|
||
|
return "pkexec"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
func startDovecotListener(information library.ServiceInitializationInformation, database library.Database) {
|
||
|
// Create a new SASL server
|
||
|
server := sasl.NewPlainServer(func(identity, username, password string) error {
|
||
|
// Check if the user exists
|
||
|
var idCheck []byte
|
||
|
err := database.DB.QueryRow("SELECT id FROM users WHERE id = $1", getUUIDBytes(username)).Scan(&idCheck)
|
||
|
if err != nil || !bytes.Equal(idCheck, getUUIDBytes(username)) {
|
||
|
return errors.New("user not found")
|
||
|
}
|
||
|
|
||
|
// Check if the password is correct
|
||
|
var dbPassword string
|
||
|
err = database.DB.QueryRow("SELECT password FROM emails WHERE owner = $1 AND isPrimary = true", getUUIDBytes(username)).Scan(&dbPassword)
|
||
|
if err != nil || dbPassword != password {
|
||
|
return errors.New("invalid password")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
|
||
|
server.Next()
|
||
|
}
|
||
|
|
||
|
func Main(information library.ServiceInitializationInformation) {
|
||
|
var doveConn *sql.DB
|
||
|
// doveConn, err := sql.Open("postgres", information.Configuration["connectionString"].(string))
|
||
|
// if err != nil {
|
||
|
// logFunc(err.Error(), 3, information)
|
||
|
// }
|
||
|
var err error
|
||
|
|
||
|
var conn library.Database
|
||
|
hostName := information.Configuration["hostName"].(string)
|
||
|
|
||
|
// Initiate a connection to the database
|
||
|
// Call service ID 1 to get the database connection information
|
||
|
information.Outbox <- library.InterServiceMessage{
|
||
|
ServiceID: ServiceInformation.ServiceID,
|
||
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), // Service initialization service
|
||
|
MessageType: 1, // Request connection information
|
||
|
SentAt: time.Now(),
|
||
|
Message: nil,
|
||
|
}
|
||
|
|
||
|
// Wait for the response
|
||
|
response := <-information.Inbox
|
||
|
if response.MessageType == 2 {
|
||
|
// This is the connection information
|
||
|
// Set up the database connection
|
||
|
conn = response.Message.(library.Database)
|
||
|
initDb(conn, information)
|
||
|
} else {
|
||
|
// This is an error message
|
||
|
// Log the error message to the logger service
|
||
|
logFunc(response.Message.(error).Error(), 3, information)
|
||
|
}
|
||
|
|
||
|
// Ask the authentication service for the public key
|
||
|
information.Outbox <- library.InterServiceMessage{
|
||
|
ServiceID: ServiceInformation.ServiceID,
|
||
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service
|
||
|
MessageType: 2, // Request public key
|
||
|
SentAt: time.Now(),
|
||
|
Message: nil,
|
||
|
}
|
||
|
|
||
|
var publicKey ed25519.PublicKey = nil
|
||
|
|
||
|
// 3 second timeout
|
||
|
go func() {
|
||
|
time.Sleep(3 * time.Second)
|
||
|
if publicKey == nil {
|
||
|
logFunc("Timeout while waiting for the public key from the authentication service", 3, information)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Wait for the response
|
||
|
response = <-information.Inbox
|
||
|
if response.MessageType == 2 {
|
||
|
// This is the public key
|
||
|
publicKey = response.Message.(ed25519.PublicKey)
|
||
|
} else {
|
||
|
// This is an error message
|
||
|
// Log the error message to the logger service
|
||
|
logFunc(response.Message.(error).Error(), 3, information)
|
||
|
}
|
||
|
|
||
|
// Ask the authentication service for the OAuth host name
|
||
|
information.Outbox <- library.InterServiceMessage{
|
||
|
ServiceID: ServiceInformation.ServiceID,
|
||
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service
|
||
|
MessageType: 0, // Request OAuth host name
|
||
|
SentAt: time.Now(),
|
||
|
Message: nil,
|
||
|
}
|
||
|
|
||
|
var oauthHostName string
|
||
|
|
||
|
// 3 second timeout
|
||
|
go func() {
|
||
|
time.Sleep(3 * time.Second)
|
||
|
if oauthHostName == "" {
|
||
|
logFunc("Timeout while waiting for the OAuth host name from the authentication service", 3, information)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Wait for the response
|
||
|
response = <-information.Inbox
|
||
|
if response.MessageType == 0 {
|
||
|
// This is the OAuth host name
|
||
|
oauthHostName = response.Message.(string)
|
||
|
} else {
|
||
|
// This is an error message
|
||
|
// Log the error message to the logger service
|
||
|
logFunc(response.Message.(error).Error(), 3, information)
|
||
|
}
|
||
|
|
||
|
// Ask the authentication service to create a new OAuth2 client
|
||
|
urlPath, err := url.JoinPath(hostName, "/oauth")
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 3, information)
|
||
|
}
|
||
|
|
||
|
information.Outbox <- library.InterServiceMessage{
|
||
|
ServiceID: ServiceInformation.ServiceID,
|
||
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000004"), // Authentication service
|
||
|
MessageType: 1, // Create OAuth2 client
|
||
|
SentAt: time.Now(),
|
||
|
Message: nucleusLibrary.OAuthInformation{
|
||
|
Name: "Kittemail",
|
||
|
RedirectUri: urlPath,
|
||
|
KeyShareUri: "",
|
||
|
Scopes: []string{"openid"},
|
||
|
},
|
||
|
}
|
||
|
|
||
|
var oauthResponse nucleusLibrary.OAuthResponse
|
||
|
|
||
|
// 3 second timeout
|
||
|
go func() {
|
||
|
time.Sleep(3 * time.Second)
|
||
|
if oauthResponse == (nucleusLibrary.OAuthResponse{}) {
|
||
|
logFunc("Timeout while waiting for the OAuth response from the authentication service", 3, information)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Wait for the response
|
||
|
response = <-information.Inbox
|
||
|
switch response.MessageType {
|
||
|
case 0:
|
||
|
// Success, set the OAuth response
|
||
|
oauthResponse = response.Message.(nucleusLibrary.OAuthResponse)
|
||
|
logFunc("Initialized with App ID: "+oauthResponse.AppID, 0, information)
|
||
|
case 1:
|
||
|
// An error which is their fault
|
||
|
logFunc(response.Message.(error).Error(), 3, information)
|
||
|
case 2:
|
||
|
// An error which is our fault
|
||
|
logFunc(response.Message.(error).Error(), 3, information)
|
||
|
default:
|
||
|
// An unknown error
|
||
|
logFunc("Unknown error", 3, information)
|
||
|
}
|
||
|
|
||
|
// Start the Dovecot listener
|
||
|
go startDovecotListener(information, conn)
|
||
|
|
||
|
// Set up the router
|
||
|
router := information.Router
|
||
|
|
||
|
// Set up the static routes
|
||
|
staticDir, err := fs.Sub(information.ResourceDir, "static")
|
||
|
if err != nil {
|
||
|
logFunc(err.Error(), 3, information)
|
||
|
} else {
|
||
|
router.Handle("/kt-static/*", http.StripPrefix("/kt-static/", http.FileServerFS(staticDir)))
|
||
|
}
|
||
|
|
||
|
// Set up the API routes
|
||
|
router.Post("/api/signup", func(w http.ResponseWriter, r *http.Request) {
|
||
|
var request struct {
|
||
|
Username string `json:"username"`
|
||
|
Domain string `json:"domain"`
|
||
|
Token string `json:"token"`
|
||
|
Storage float64 `json:"storage"`
|
||
|
}
|
||
|
err := json.NewDecoder(r.Body).Decode(&request)
|
||
|
if err != nil {
|
||
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid request"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if request.Storage < 0 {
|
||
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid storage"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Validate if the username is valid
|
||
|
if len(request.Username) < 3 || len(request.Username) > 20 {
|
||
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid username"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Validate if the domain is valid
|
||
|
switch request.Domain {
|
||
|
case "cta.social", "ailur.dev":
|
||
|
break
|
||
|
default:
|
||
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid domain"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Check if the user is authenticated
|
||
|
claims, ok := verifyJwt(request.Token, publicKey, conn, false)
|
||
|
if !ok {
|
||
|
renderJSON(401, w, map[string]interface{}{"error": "Invalid token"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Check if the user already exists
|
||
|
userId := uuid.MustParse(claims["sub"].(string))
|
||
|
var idCheck []byte
|
||
|
err = conn.DB.QueryRow("SELECT id FROM users WHERE id = $1", userId[:]).Scan(&idCheck)
|
||
|
if err == nil && bytes.Equal(idCheck, userId[:]) {
|
||
|
renderJSON(409, w, map[string]interface{}{"error": "User already exists"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Check if the email already exists
|
||
|
var emailCheck string
|
||
|
err = conn.DB.QueryRow("SELECT address FROM emails WHERE username = $1 AND address = $2", request.Username, request.Domain).Scan(&emailCheck)
|
||
|
if err == nil && emailCheck == request.Domain {
|
||
|
renderJSON(409, w, map[string]interface{}{"error": "Email already exists"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Reserve the space for the user
|
||
|
information.Outbox <- library.InterServiceMessage{
|
||
|
ServiceID: ServiceInformation.ServiceID,
|
||
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000003"), // Blob storage service
|
||
|
MessageType: 2, // Reserve space
|
||
|
SentAt: time.Now(),
|
||
|
Message: nucleusLibrary.Quota{
|
||
|
User: userId,
|
||
|
Bytes: int64(request.Storage),
|
||
|
},
|
||
|
}
|
||
|
|
||
|
// 3 second timeout
|
||
|
var fileSpaceReserved bool
|
||
|
go func() {
|
||
|
time.Sleep(3 * time.Second)
|
||
|
if !fileSpaceReserved {
|
||
|
logFunc("Timeout while waiting for the file space to be reserved", 3, information)
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "00"}, information)
|
||
|
// Panic to stop the function
|
||
|
panic("Timeout while waiting for the file space to be reserved")
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Wait for the response
|
||
|
response = <-information.Inbox
|
||
|
if response.MessageType == 0 {
|
||
|
// Success
|
||
|
fileSpaceReserved = true
|
||
|
} else if response.MessageType == 1 {
|
||
|
// An error
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "01"}, information)
|
||
|
return
|
||
|
} else if response.MessageType == 2 {
|
||
|
// Invalid request
|
||
|
renderJSON(400, w, map[string]interface{}{"error": response.Message.(error).Error()}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Insert the user into the database
|
||
|
_, err = conn.DB.Exec("INSERT INTO users (id, subscription) VALUES ($1, 0)", userId[:])
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "02"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Insert the email into the database
|
||
|
password, err := randomChars(16)
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "03"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, err = conn.DB.Exec("INSERT INTO emails (username, address, isPrimary, owner, password) VALUES ($1, $2, true, $3, $4)", request.Username, request.Domain, userId[:], password)
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "04"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Contact dovecot to create the user
|
||
|
_, err = doveConn.Exec("INSERT INTO users (email, maildir, quota_rule) VALUES ($1, $2, $3)", request.Username+"@"+request.Domain, userId.String(), "*:storage="+strconv.FormatInt(int64(request.Storage), 10)+"B", userId)
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "05"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
renderJSON(200, w, map[string]interface{}{"password": password}, information)
|
||
|
})
|
||
|
|
||
|
router.Post("/api/email/info", func(w http.ResponseWriter, r *http.Request) {
|
||
|
var request struct {
|
||
|
Token string `json:"token"`
|
||
|
Username string `json:"username"`
|
||
|
Address string `json:"address"`
|
||
|
}
|
||
|
err := json.NewDecoder(r.Body).Decode(&request)
|
||
|
if err != nil {
|
||
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid request"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Check if the user is authenticated
|
||
|
claims, ok := verifyJwt(request.Token, publicKey, conn, true)
|
||
|
if !ok {
|
||
|
renderJSON(401, w, map[string]interface{}{"error": "Invalid token"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Get the email information
|
||
|
var username, address, password string
|
||
|
var isPrimary bool
|
||
|
err = conn.DB.QueryRow("SELECT username, address, password, isPrimary FROM emails WHERE username = $1 AND address = $2 AND owner = $3", request.Username, request.Address, getUUIDBytes(claims["sub"].(string))).Scan(&username, &address, &password, &isPrimary)
|
||
|
if err != nil {
|
||
|
renderJSON(404, w, map[string]interface{}{"error": "Email not found"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Return the email information
|
||
|
renderJSON(200, w, map[string]interface{}{
|
||
|
"username": username,
|
||
|
"address": address,
|
||
|
"password": password,
|
||
|
"isPrimary": isPrimary,
|
||
|
}, information)
|
||
|
})
|
||
|
|
||
|
router.Post("/api/email/resetLoginCode", func(w http.ResponseWriter, r *http.Request) {
|
||
|
var request struct {
|
||
|
Token string `json:"token"`
|
||
|
Username string `json:"username"`
|
||
|
Address string `json:"address"`
|
||
|
}
|
||
|
err := json.NewDecoder(r.Body).Decode(&request)
|
||
|
if err != nil {
|
||
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid request"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Check if the user is authenticated
|
||
|
claims, ok := verifyJwt(request.Token, publicKey, conn, true)
|
||
|
if !ok {
|
||
|
renderJSON(401, w, map[string]interface{}{"error": "Invalid token"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Generate a new password
|
||
|
password, err := randomChars(16)
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "05"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Change the password
|
||
|
_, err = conn.DB.Exec("UPDATE emails SET password = $1 WHERE username = $2 AND address = $3 AND owner = $4", password, request.Username, request.Address, getUUIDBytes(claims["sub"].(string)))
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "06"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
renderJSON(200, w, nil, information)
|
||
|
})
|
||
|
|
||
|
router.Post("/api/email/delete", func(w http.ResponseWriter, r *http.Request) {
|
||
|
var request struct {
|
||
|
Token string `json:"token"`
|
||
|
Username string `json:"username"`
|
||
|
Address string `json:"address"`
|
||
|
}
|
||
|
err := json.NewDecoder(r.Body).Decode(&request)
|
||
|
if err != nil {
|
||
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid request"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Check if the user is authenticated
|
||
|
claims, ok := verifyJwt(request.Token, publicKey, conn, true)
|
||
|
if !ok {
|
||
|
renderJSON(401, w, map[string]interface{}{"error": "Invalid token"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
userID := uuid.MustParse(claims["sub"].(string))
|
||
|
|
||
|
// Check if it's the isPrimary email
|
||
|
var isPrimary bool
|
||
|
err = conn.DB.QueryRow("SELECT isPrimary FROM emails WHERE username = $1 AND address = $2 AND owner = $3", request.Username, request.Address, userID[:]).Scan(&isPrimary)
|
||
|
if err != nil {
|
||
|
renderJSON(404, w, map[string]interface{}{"error": "Email not found"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if !isPrimary {
|
||
|
// Delete the email
|
||
|
_, err = conn.DB.Exec("DELETE FROM emails WHERE username = $1 AND address = $2 AND owner = $3", request.Username, request.Address, userID[:])
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "07"}, information)
|
||
|
return
|
||
|
}
|
||
|
} else {
|
||
|
// Get the user's quota
|
||
|
var quota string
|
||
|
err = doveConn.QueryRow("SELECT quota_rule FROM users WHERE id = $1", userID.String()).Scan("a)
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "08"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Parse the quota
|
||
|
quotaInt, err := strconv.ParseInt(strings.TrimSuffix(strings.TrimPrefix(quota, "*:storage="), "B"), 10, 64)
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "09"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Delete the maildir
|
||
|
_, err = doveConn.Exec("DELETE FROM users WHERE maildir = $1", userID.String())
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "08"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
err = os.RemoveAll(filepath.Join("/home/mailboxes/maildir/", userID.String()))
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "09"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Delete the emails
|
||
|
_, err = conn.DB.Exec("DELETE FROM emails WHERE owner = $1", userID[:])
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "0A"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Delete the user
|
||
|
_, err = conn.DB.Exec("DELETE FROM users WHERE id = $1", userID[:])
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "0B"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Release the space
|
||
|
information.Outbox <- library.InterServiceMessage{
|
||
|
ServiceID: ServiceInformation.ServiceID,
|
||
|
ForServiceID: uuid.MustParse("00000000-0000-0000-0000-000000000003"), // Blob storage service
|
||
|
MessageType: 2, // Add space, but we can use a negative number to release space
|
||
|
SentAt: time.Now(),
|
||
|
Message: nucleusLibrary.Quota{
|
||
|
User: userID,
|
||
|
Bytes: -quotaInt,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
// 3 second timeout
|
||
|
var fileSpaceReleased bool
|
||
|
go func() {
|
||
|
time.Sleep(3 * time.Second)
|
||
|
if !fileSpaceReleased {
|
||
|
logFunc("Timeout while waiting for the file space to be released", 3, information)
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "0C"}, information)
|
||
|
// Panic to stop the function
|
||
|
panic("Timeout while waiting for the file space to be released")
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Wait for the response
|
||
|
response = <-information.Inbox
|
||
|
if response.MessageType == 0 {
|
||
|
// Success
|
||
|
fileSpaceReleased = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
renderJSON(200, w, nil, information)
|
||
|
})
|
||
|
|
||
|
router.Post("/api/email/list", func(w http.ResponseWriter, r *http.Request) {
|
||
|
var request struct {
|
||
|
Token string `json:"token"`
|
||
|
}
|
||
|
err := json.NewDecoder(r.Body).Decode(&request)
|
||
|
if err != nil {
|
||
|
renderJSON(400, w, map[string]interface{}{"error": "Invalid request"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Check if the user is authenticated
|
||
|
claims, ok := verifyJwt(request.Token, publicKey, conn, true)
|
||
|
if !ok {
|
||
|
renderJSON(401, w, map[string]interface{}{"error": "Invalid token"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Get the emails
|
||
|
rows, err := conn.DB.Query("SELECT username, address, isPrimary FROM emails WHERE owner = $1", getUUIDBytes(claims["sub"].(string)))
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "0D"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
var emails []map[string]interface{}
|
||
|
for rows.Next() {
|
||
|
var username, address string
|
||
|
var isPrimary bool
|
||
|
err = rows.Scan(&username, &address, &isPrimary)
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "0E"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
emails = append(emails, map[string]interface{}{
|
||
|
"username": username,
|
||
|
"address": address,
|
||
|
"isPrimary": isPrimary,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
err = rows.Close()
|
||
|
if err != nil {
|
||
|
renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "0F"}, information)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
renderJSON(200, w, emails, information)
|
||
|
})
|
||
|
|
||
|
// TODO: Once OAuth2 RFC is approved and all the EMail clients support it, switch out the Email-specific password for OAuth2
|
||
|
// TODO: Add automatic alias addition, deletion and listing
|
||
|
// TODO: Add subscription management when Ailur becomes incorporated
|
||
|
|
||
|
// Set up the routes
|
||
|
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||
|
renderTemplate(200, w, nil, "index.html", information)
|
||
|
})
|
||
|
|
||
|
router.Get("/signup", func(w http.ResponseWriter, r *http.Request) {
|
||
|
renderTemplate(200, w, nil, "signup.html", information)
|
||
|
})
|
||
|
|
||
|
router.Get("/oauth", func(w http.ResponseWriter, r *http.Request) {
|
||
|
renderTemplate(200, w, map[string]interface{}{
|
||
|
"ClientId": oauthResponse.AppID,
|
||
|
"AuthorizationUri": oauthHostName,
|
||
|
}, "oauth.html", information)
|
||
|
})
|
||
|
|
||
|
router.Get("/settings", func(w http.ResponseWriter, r *http.Request) {
|
||
|
renderTemplate(200, w, nil, "settings.html", information)
|
||
|
})
|
||
|
|
||
|
router.Get("/quickstart", func(w http.ResponseWriter, r *http.Request) {
|
||
|
renderTemplate(200, w, nil, "quickstart.html", information)
|
||
|
})
|
||
|
|
||
|
router.Get("/autoconfig.xml", func(w http.ResponseWriter, r *http.Request) {
|
||
|
renderTemplate(200, w, nil, "autoconfig.xml", information)
|
||
|
})
|
||
|
}
|