// Copyright ©2021 The episto Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package episto // import "sbinet.org/x/episto"
import (
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-vcard"
"github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/carddav"
uuid "github.com/hashicorp/go-uuid"
"gopkg.in/ini.v1"
)
type Account struct {
Name string
User string `ini:"user"`
Cred string `ini:"cred-cmd"`
URL string `ini:"url"`
cli *carddav.Client
}
func LoadAccounts() ([]Account, error) {
dir, err := os.UserConfigDir()
if err != nil {
return nil, fmt.Errorf("could not find user config directory: %w", err)
}
f, err := ini.Load(filepath.Join(dir, "episto", "accounts.conf"))
if err != nil {
return nil, fmt.Errorf("could not find config file: %w", err)
}
var cfg []Account
for _, name := range f.SectionStrings() {
if name == "DEFAULT" {
continue
}
sec := f.Section(name)
var acc Account
err := sec.MapTo(&acc)
if err != nil {
return nil, fmt.Errorf("could not read section %q: %w", name, err)
}
acc.Name = name
acc.Cred, err = loadCreds(acc.Cred)
if err != nil {
return nil, fmt.Errorf("could not load credentials for section %q: %w", name, err)
}
cfg = append(cfg, acc)
}
return cfg, nil
}
func loadCreds(c string) (string, error) {
cmd := exec.Command("sh", "-c", c)
cmd.Stdin = os.Stdin
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("could not read password: %w", err)
}
return strings.TrimSpace(string(out)), nil
}
func (acc *Account) dial() error {
cli, err := carddav.NewClient(
webdav.HTTPClientWithBasicAuth(http.DefaultClient, acc.User, acc.Cred),
acc.URL,
)
if err != nil {
return fmt.Errorf("could not create CardDAV client: %w", err)
}
acc.cli = cli
return nil
}
func (acc *Account) List(qry string) ([]mail.Address, error) {
if acc.cli == nil {
err := acc.dial()
if err != nil {
return nil, fmt.Errorf("could not dial CardDAV server: %w", err)
}
}
principal, err := acc.cli.FindCurrentUserPrincipal()
if err != nil {
return nil, fmt.Errorf("could not find principal: %w", err)
}
homeSet, err := acc.cli.FindAddressBookHomeSet(principal)
if err != nil {
return nil, fmt.Errorf("failed to query CardDAV address book home set: %w", err)
}
addressBooks, err := acc.cli.FindAddressBooks(homeSet)
if err != nil {
return nil, fmt.Errorf("failed to query CardDAV address books: %w", err)
}
query := &carddav.AddressBookQuery{
DataRequest: carddav.AddressDataRequest{
Props: []string{
vcard.FieldFormattedName,
vcard.FieldEmail,
vcard.FieldUID,
},
},
PropFilters: []carddav.PropFilter{{
Name: vcard.FieldFormattedName,
TextMatches: []carddav.TextMatch{{Text: ""}},
}},
}
query.PropFilters = nil
query.PropFilters = append(query.PropFilters, []carddav.PropFilter{
{
Name: vcard.FieldFormattedName,
TextMatches: []carddav.TextMatch{{Text: qry}},
},
{
Name: vcard.FieldEmail,
TextMatches: []carddav.TextMatch{{Text: qry}},
},
}...)
var results []mail.Address
for _, addrBook := range addressBooks {
addrs, err := acc.cli.QueryAddressBook(addrBook.Path, query)
if err != nil {
return nil, fmt.Errorf("failed to query CardDAV addresses: %w", err)
}
for _, addr := range addrs {
name := addr.Card.Value(vcard.FieldFormattedName)
cardEmails := addr.Card.Values(vcard.FieldEmail)
for _, v := range cardEmails {
results = append(results, mail.Address{
Name: name,
Address: v,
})
}
}
}
return results, nil
}
func (acc *Account) Push(card vcard.Card) (*carddav.AddressObject, error) {
if acc.cli == nil {
err := acc.dial()
if err != nil {
return nil, fmt.Errorf("could not dial CardDAV server: %w", err)
}
}
var (
id string
err error
)
switch field := card.Get(vcard.FieldUID); field {
case nil:
id, err = uuid.GenerateUUID()
if err != nil {
return nil, fmt.Errorf("could not generate UUID: %w", err)
}
default:
id = field.Value
}
path := filepath.ToSlash(id + ".vcf")
addrObj, err := acc.cli.PutAddressObject(path, card)
if err != nil {
return nil, fmt.Errorf("could not put address object to %q: %w", path, err)
}
return addrObj, nil
}