added connection of mail accounts
jump to
@@ -2,15 +2,11 @@ package main
import ( "context" - "errors" - "strings" "github.com/adrg/xdg" - _ "github.com/emersion/go-imap/v2" - "github.com/emersion/go-imap/v2/imapclient" - "github.com/zalando/go-keyring" "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type AppConfig struct {@@ -30,16 +26,18 @@ db, err := gorm.Open(sqlite.Open(app.GetDataFile()), &gorm.Config{})
if err != nil { panic(err) } - app.DB = db + app.DB = db.Session(&gorm.Session{FullSaveAssociations: true}) app.migrate() return app } func (app *App) startup(ctx context.Context) { app.ctx = ctx + app.EstablishMailConnections() } func (app *App) shutdown(ctx context.Context) { + CloseMailConnections() return }@@ -51,10 +49,12 @@ app.DB.AutoMigrate(&URL{})
app.DB.AutoMigrate(&StreetAddress{}) app.DB.AutoMigrate(&Date{}) app.DB.AutoMigrate(&OtherField{}) - app.DB.FirstOrCreate( - &app.UserContact, - Contact{Model: gorm.Model{ID: 1}}, - ) + app.DB.Preload(clause.Associations). + FirstOrCreate( + &app.UserContact, + Contact{Model: gorm.Model{ID: 1}}, + ) + app.DB.AutoMigrate(&MailAccountConnection{}) } func (app *App) GetTitle() string {@@ -69,60 +69,7 @@ return file
} } -func (app *App) GetMailServerKeyringService() string { - title := app.GetTitle() - return title + "-mail-server" -} - -func (app *App) GetMailPasswordKeyringService() string { +func (app *App) GetMailPasswordKeyringService() string { title := app.GetTitle() return title + "-mail-password" } - -type MailConnection struct { - Client *imapclient.Client - err error -} -var mailConnections = make(map[string]MailConnection) - -func (app *App) UserMailAuth() { - for _, email := range app.UserContact.Emails { - app.MailAuth(email) - } -} - -func (app *App) MailAuth(email EmailAddress) (conn MailConnection) { - mailServer, err := keyring.Get(app.GetMailServerKeyringService(), email.Value) - if err != nil { - conn.err = err - return - } - if mailServer == "" { - conn.err = errors.New("No mail server found") - return - } - - mailPassword, err := keyring.Get(app.GetMailPasswordKeyringService(), email.Value) - if err != nil { - conn.err = err - return - } - if mailPassword == "" { - conn.err = errors.New("No password found") - return - } - - client, err := imapclient.DialTLS(mailServer + ":993", nil) - if err != nil { - conn.err = err - return - } - conn.Client = client - - if err := client.Login(strings.Split(email.Value, "@")[0], mailPassword).Wait(); err != nil { - conn.err = err - return - } - - return -}
@@ -42,8 +42,7 @@ }
if contact.ID == 1 { app.UserContact = contact } - app.DB.Save(contact) - return nil + return app.DB.Save(&contact).Error } type EmailAddress struct {
@@ -2,6 +2,6 @@ <script lang="ts">
let { icon, ...rest } = $props(); </script> -<button class="cursor-pointer w-4 fill-control stroke-control" +<button class="w-4 fill-control stroke-control" {...rest}>{@html icon}</button>
@@ -2,20 +2,18 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT import {main} from '../models'; -export function GetAppConfigFile():Promise<string>; +export function EstablishMailConnections():Promise<void>; export function GetDataFile():Promise<string>; -export function GetMailPasswordKeyringService():Promise<string>; +export function GetMailConnections():Promise<Array<main.MailAccountConnection>>; -export function GetMailServerKeyringService():Promise<string>; +export function GetMailPasswordKeyringService():Promise<string>; export function GetTitle():Promise<string>; export function GetUserContact():Promise<main.Contact>; -export function MailAuth(arg1:main.EmailAddress):Promise<main.MailConnection>; - export function SaveContact(arg1:main.Contact):Promise<void>; -export function UserMailAuth():Promise<void>; +export function SaveMailConnection(arg1:main.MailAccountConnection):Promise<void>;
@@ -2,20 +2,20 @@ // @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export function GetAppConfigFile() { - return window['go']['main']['App']['GetAppConfigFile'](); +export function EstablishMailConnections() { + return window['go']['main']['App']['EstablishMailConnections'](); } export function GetDataFile() { return window['go']['main']['App']['GetDataFile'](); } -export function GetMailPasswordKeyringService() { - return window['go']['main']['App']['GetMailPasswordKeyringService'](); +export function GetMailConnections() { + return window['go']['main']['App']['GetMailConnections'](); } -export function GetMailServerKeyringService() { - return window['go']['main']['App']['GetMailServerKeyringService'](); +export function GetMailPasswordKeyringService() { + return window['go']['main']['App']['GetMailPasswordKeyringService'](); } export function GetTitle() {@@ -26,14 +26,10 @@ export function GetUserContact() {
return window['go']['main']['App']['GetUserContact'](); } -export function MailAuth(arg1) { - return window['go']['main']['App']['MailAuth'](arg1); -} - export function SaveContact(arg1) { return window['go']['main']['App']['SaveContact'](arg1); } -export function UserMailAuth() { - return window['go']['main']['App']['UserMailAuth'](); +export function SaveMailConnection(arg1) { + return window['go']['main']['App']['SaveMailConnection'](arg1); }
@@ -344,17 +344,48 @@ }
} - export class MailConnection { + export class MailAccountConnection { + ID: number; + // Go type: time + CreatedAt: any; + // Go type: time + UpdatedAt: any; + // Go type: gorm + DeletedAt: any; + Email: string; + ImapServer: string; + ImapPort: number; + ImapUsername: string; + SmtpServer: string; + SmtpPort: number; + SmtpUsername: string; + Password: string; // Go type: imapclient - Client?: any; + ImapClient?: any; + SmtpClient?: smtp.Client; + SmtpSasl?: any; static createFrom(source: any = {}) { - return new MailConnection(source); + return new MailAccountConnection(source); } constructor(source: any = {}) { if ('string' === typeof source) source = JSON.parse(source); - this.Client = this.convertValues(source["Client"], null); + this.ID = source["ID"]; + this.CreatedAt = this.convertValues(source["CreatedAt"], null); + this.UpdatedAt = this.convertValues(source["UpdatedAt"], null); + this.DeletedAt = this.convertValues(source["DeletedAt"], null); + this.Email = source["Email"]; + this.ImapServer = source["ImapServer"]; + this.ImapPort = source["ImapPort"]; + this.ImapUsername = source["ImapUsername"]; + this.SmtpServer = source["SmtpServer"]; + this.SmtpPort = source["SmtpPort"]; + this.SmtpUsername = source["SmtpUsername"]; + this.Password = source["Password"]; + this.ImapClient = this.convertValues(source["ImapClient"], null); + this.SmtpClient = this.convertValues(source["SmtpClient"], smtp.Client); + this.SmtpSasl = source["SmtpSasl"]; } convertValues(a: any, classs: any, asMap: boolean = false): any {@@ -381,3 +412,24 @@
} +export namespace smtp { + + export class Client { + CommandTimeout: number; + SubmissionTimeout: number; + DebugWriter: any; + + static createFrom(source: any = {}) { + return new Client(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.CommandTimeout = source["CommandTimeout"]; + this.SubmissionTimeout = source["SubmissionTimeout"]; + this.DebugWriter = source["DebugWriter"]; + } + } + +} +
@@ -7,7 +7,7 @@ import { global } from "$lib/global.svelte.js";
let showJob = $derived(global.contact.Job !== "") - function preventDefault(event) { + async function preventDefault(event) { event.preventDefault(); global.contact.Emails = (global.contact.Emails || []).filter(x => x.Key && x.Value); global.contact.PhoneNumbers = (global.contact.PhoneNumbers || []).filter(x => x.Key && x.Value);@@ -15,7 +15,7 @@ global.contact.Urls = (global.contact.Urls || []).filter(x => x.Key && x.Value);
global.contact.StreetAddresses = (global.contact.StreetAddresses || []).filter(x => x.Key && (x.Street1 || x.Street2 || x.City || x.State || x.ZipCode || x.Country)); global.contact.Dates = (global.contact.Dates || []).filter(x => x.Key && x.Value); global.contact.Other = (global.contact.Other || []).filter(x => x.Key && x.Value); - SaveContact(global.contact).catch(err => { + await SaveContact(global.contact).catch(err => { alert(err); return; });@@ -122,26 +122,5 @@ {/each}
</fieldset> <AddField fieldName="Other" onclick={add_other} /> </section> - <input class="ml-auto" type="submit" value="Done" /> + <input class="ml-auto text-button" type="submit" value="Done" /> </form> - -<style> - fieldset { - display: flex; - flex-direction: row; - gap: 16px; - align-items: end; - } - - .field-group { - background-color: var(--color-header); - padding: 16px 20px; - display: flex; - flex-direction: column; - gap: 16px; - align-items: start !important; - } - .field-group:empty { - display: none; - } -</style>
@@ -303,3 +303,26 @@ --color-control: var(--color-neutral-400);
--color-input: var(--color-neutral-300); --color-button: var(--color-slate-400) } + +button, input[type="submit"] { + cursor: pointer; +} + +fieldset { + display: flex; + flex-direction: row; + gap: 16px; + align-items: end; +} + +.field-group { + background-color: var(--color-header); + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 16px; + align-items: start !important; +} +.field-group:empty { + display: none; +}
@@ -0,0 +1,59 @@
+<script lang="ts"> + import GenericInput from "$lib/components/GenericInput.svelte"; + import { goto } from "$app/navigation"; + import { GetMailConnections, SaveMailConnection, EstablishMailConnections } from "$lib/wailsjs/go/main/App"; + import { global } from "$lib/global.svelte.js"; + + const connections = await GetMailConnections(); + + function edit_contact_card() { + global.contact = userContact; + goto("/contact"); + } + + async function preventDefault(event) { + event.preventDefault(); + for (const conn of connections) { + await SaveMailConnection(conn).catch(err => { + alert(err); + return; + }); + } + EstablishMailConnections(); + goto("/settings"); + } +</script> + +<form class="flex flex-col p-4 w-full h-full overflow-scroll" onsubmit={preventDefault}> + <section class="flex flex-col gap-8 text-xs w-1/2"> + <h1>Mail Account Settings</h1> + {#each connections as conn} + <h2>Account Settings for <span class="font-bold">{conn.Email}</span></h2> + <fieldset class="field-group"> + <section class="flex flex-col gap-4"> + <h3>IMAP Settings</h3> + <fieldset> + <label>Server <GenericInput bind:value={conn.ImapServer} /></label> + <label>Port <GenericInput type="number" bind:value={conn.ImapPort} /></label> + </fieldset> + <label>Username <GenericInput bind:value={conn.ImapUsername} /></label> + </section> + <section class="flex flex-col gap-4"> + <h3>SMTP Settings</h3> + <fieldset> + <label>Server <GenericInput bind:value={conn.SmtpServer} /></label> + <label>Port <GenericInput type="number" bind:value={conn.SmtpPort} /></label> + </fieldset> + <label>Username <GenericInput bind:value={conn.SmtpUsername} /></label> + </section> + <label>Password <GenericInput type="password" bind:value={conn.Password} /></label> + </fieldset> + {:else} + <h1 class="text-xl">You have no mail accounts setup. Edit your contact card and add mail accounts.</h1> + <button class="text-button" + type="button" + onclick={edit_contact_card}>Click here to edit contact card</button> + {/each} + </section> + <input class="ml-auto text-button" type="submit" value="Done" /> +</form>
@@ -1,1 +1,5 @@
-<h1>Settings</h1> +<div class="p-4"> + <h1>Settings</h1> + + <a class="text-button" href="/mailaccount">Configure Mail Accounts</a> +</div>
@@ -5,6 +5,8 @@
require ( github.com/adrg/xdg v0.5.3 github.com/emersion/go-imap/v2 v2.0.0-beta.8 + github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 + github.com/emersion/go-smtp v0.24.0 github.com/wailsapp/wails/v2 v2.12.0 github.com/zalando/go-keyring v0.2.8 gorm.io/driver/sqlite v1.6.0@@ -16,7 +18,6 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
github.com/bep/debounce v1.2.1 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/emersion/go-message v0.18.2 // indirect - github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/google/uuid v1.6.0 // indirect
@@ -14,6 +14,8 @@ github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg=
github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk= +github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
@@ -0,0 +1,181 @@
+package main + +import ( + "log" + "strconv" + + "github.com/emersion/go-imap/v2/imapclient" + "github.com/emersion/go-sasl" + "github.com/emersion/go-smtp" + "github.com/zalando/go-keyring" + "gorm.io/gorm" +) + +type MailAccountConnection struct { + gorm.Model + Email string `gorm:"uniqueIndex"` + ImapServer string + ImapPort int + ImapUsername string + SmtpServer string + SmtpPort int + SmtpUsername string + Password string `gorm:"-:all"` + ImapClient *imapclient.Client `gorm:"-:all"` + SmtpClient *smtp.Client `gorm:"-:all"` + SmtpSasl *sasl.Client `gorm:"-:all"` +} + +var mailConnections = make(map[string]MailAccountConnection) + +func (app *App) GetMailConnections() ([]MailAccountConnection, error) { + userContact := app.GetUserContact() + connections := make([]MailAccountConnection, len(userContact.Emails)) + for index, email := range userContact.Emails { + if err := app.DB. + Where("email = ?", email.Value). + First(&connections[index]). + Error; err != nil { + connections[index].Email = email.Value + connections[index].ImapUsername = email.Value + connections[index].ImapPort = 993 + connections[index].SmtpUsername = email.Value + connections[index].SmtpPort = 465 + continue + } + mailPassword, err := keyring.Get( + app.GetMailPasswordKeyringService(), + email.Value, + ) + if err != nil { + continue + } + connections[index].Password = mailPassword + + if connections[index].Email != "" && + connections[index].ImapServer != "" && + connections[index].ImapPort != 0 && + connections[index].ImapUsername != "" && + connections[index].SmtpServer != "" && + connections[index].SmtpPort != 0 && + connections[index].SmtpUsername != "" && + connections[index].Password != "" { + mailConnections[email.Value] = connections[index] + } + } + + return connections, nil +} + +func (app *App) SaveMailConnection(conn MailAccountConnection) error { + keyring.Set( + app.GetMailPasswordKeyringService(), + conn.Email, + conn.Password, + ) + return app.DB.Save(&conn).Error +} + +func RemoveMailConnection(address string) { + if conn, ok := mailConnections[address]; ok { + if conn.ImapClient != nil { + conn.ImapClient.Close() + } + if conn.SmtpClient != nil { + conn.SmtpClient.Close() + } + delete(mailConnections, address) + } +} + +func (app *App) EstablishMailConnections() { + _, _ = app.GetMailConnections() + for key, conn := range mailConnections { + log.Printf("Attempting to connect to %s...\n", conn.Email) + var err error + + if conn.ImapClient != nil { + conn.ImapClient.Close() + } + switch conn.ImapPort { + case 143: + conn.ImapClient, err = imapclient.DialStartTLS( + conn.ImapServer+":"+strconv.Itoa(conn.ImapPort), + nil, + ) + break + case 993: + conn.ImapClient, err = imapclient.DialTLS( + conn.ImapServer+":"+strconv.Itoa(conn.ImapPort), + nil, + ) + break + default: + conn.ImapClient, err = imapclient.DialTLS( + conn.ImapServer+":"+strconv.Itoa(conn.ImapPort), + nil, + ) + break + } + if err != nil { + RemoveMailConnection(conn.Email) + log.Print(err.Error()) + continue + } + + if err := conn.ImapClient.Login(conn.ImapUsername, conn.Password).Wait(); err != nil { + RemoveMailConnection(conn.Email) + log.Print(err.Error()) + continue + } + + if conn.SmtpClient != nil { + conn.SmtpClient.Close() + } + switch conn.SmtpPort { + case 25: + conn.SmtpClient, err = smtp.Dial(conn.SmtpServer + ":" + strconv.Itoa(conn.SmtpPort)) + break + case 587: + conn.SmtpClient, err = smtp.DialStartTLS( + conn.SmtpServer+":"+strconv.Itoa(conn.SmtpPort), + nil, + ) + break + case 465: + conn.SmtpClient, err = smtp.DialTLS( + conn.SmtpServer+":"+strconv.Itoa(conn.SmtpPort), + nil, + ) + break + default: + conn.SmtpClient, err = smtp.DialTLS( + conn.SmtpServer+":"+strconv.Itoa(conn.SmtpPort), + nil, + ) + break + } + if err != nil { + RemoveMailConnection(conn.Email) + log.Print(err.Error()) + continue + } + + auth := sasl.NewPlainClient("", conn.SmtpUsername, conn.Password) + if err := conn.SmtpClient.Auth(auth); err != nil { + RemoveMailConnection(conn.Email) + log.Print(err.Error()) + continue + } + conn.SmtpSasl = &auth + + mailConnections[key] = conn + log.Printf("Successfully connected %s!\n", conn.Email) + } +} + +func CloseMailConnections() { + for key, _ := range mailConnections { + RemoveMailConnection(key) + } +}