Skip to content

Commit

Permalink
CardDav 🚀
Browse files Browse the repository at this point in the history
- CardDav submission type (create vCard contacts on form submission)
- [BREAKING] restructure config to support multiple form types
- lots of refactorings...
  • Loading branch information
denyskon committed Jan 23, 2024
1 parent e35fe20 commit 07e0ecd
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 230 deletions.
42 changes: 42 additions & 0 deletions carddav/carddav.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 Denys Konovalov <[email protected]>

package carddav

import (
"context"
"net/http"

forms_service "github.com/denyskon/postalion/form"
"github.com/emersion/go-vcard"
"github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/carddav"
)

func HandleForm(form *forms_service.Form, req *http.Request) error {
config := form.CardDavConfig

c, err := carddav.NewClient(
webdav.HTTPClientWithBasicAuth(nil, config.Username, config.Password),
config.DavEndpoint,
)
if err != nil {
return err
}

card := vcard.Card{
"VERSION": []*vcard.Field{{Value: "3.0"}},
}

for name, field := range config.Fields {
card.SetValue(name, field.GetFormattedString(req))
}

path := config.ContactPath.GetFormattedString(req)
ctx := context.Background()
if _, err = c.PutAddressObject(ctx, path, card); err != nil {
return err
}

return nil
}
51 changes: 0 additions & 51 deletions email/email.go

This file was deleted.

74 changes: 51 additions & 23 deletions example.postalion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,58 @@ mailAccounts:
encryption: starttls
forms:
test-form:
account: test-account
from: "Postalion <[email protected]>"
to: "Contact <[email protected]>"
type: mail
honeypot: honeypot
replyTo:
format: "%s <%s>"
fields:
- name
- email
subjectField: subject
template: default
redirect: https://example.com/pages/contact-redirect
fields:
- name: name
label: Name
type: string
- name: email
label: Email
type: string
- name: subject
label: Subject
type: string
- name: file
label: File
type: file
mailConfig:
account: test-account
from: "Postalion <[email protected]>"
to: "Contact <[email protected]>"
replyTo:
format: "%s <%s>"
fields:
- name
- email
subjectField: subject
template: default
fields:
- name: name
label: Name
type: string
- name: email
label: Email
type: string
- name: subject
label: Subject
type: string
- name: file
label: File
type: file
carddav-test:
type: carddav
cardDavConfig:
davEndpoint: https://mail.example.com
contactPath:
format: "/SOGo/dav/[email protected]/Contacts/private/%s-%s.vcf"
fields:
- familyname
- givenname
username: [email protected]
password: p@ssword
fields:
"N":
format: "%s;%s"
fields:
- familyname
- givenname
"EMAIL":
format: "%s"
fields:
- email
"NOTE":
format: "Submitted on %s"
fields:
- date
redirect: https://example.com/pages/carddav-redirect
port: 8080
templateDir: /app/data/templates
108 changes: 30 additions & 78 deletions form/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,111 +7,63 @@ import (
"bytes"
"fmt"
"html/template"
"io"
"net/http"
"slices"

mail "github.com/xhit/go-simple-mail/v2"
)

type Form struct {
Account string
Type string `validate:"required,oneof=mail carddav"`
Honeypot string
Redirect string `validate:"required,http_url"`
CardDavConfig *CardDavConfig `validate:"required_if=Type carddav"`
MailConfig *MailConfig `validate:"required_if=Type mail"`
}

type CardDavConfig struct {
DavEndpoint string `validate:"required"`
ContactPath *FormattedField `validate:"required"`
Username string `validate:"required"`
Password string `validate:"required"`
Fields map[string]*FormattedField `validate:"required"`
}

type MailConfig struct {
Account string `validate:"required"`
From string `validate:"required"`
To string `validate:"required"`
Honeypot string
ReplyTo *ReplyTo
Subject string `validate:"required_without=SubjectField"`
SubjectField string
Template string `validate:"required,filepath"`
Redirect string `validate:"required,http_url"`
ReplyTo *FormattedField
Template string `validate:"required_if=Type mail"`
Fields []Field `validate:"required"`
}

type FormattedField struct {
Format string `validate:"required"`
Fields []string `validate:"required"`
}

type Field struct {
Name string `validate:"required"`
Label string `validate:"required"`
Type string `validate:"required,oneof=string file"`
}

type ReplyTo struct {
Format string `validate:"required"`
Fields []string `validate:"required"`
}

type StringField struct {
Name string
Label string
Content string
}

func (f Form) Parse(req *http.Request) ([]*StringField, []*mail.File, error) {
fieldsToParse := slices.DeleteFunc[[]Field, Field](f.Fields, func(field Field) bool {
return f.SubjectField == field.Name
})

var formContent []*StringField
var formFiles []*mail.File

for _, field := range fieldsToParse {
switch field.Type {
case "string":
{

formContent = append(formContent, &StringField{
Name: field.Name,
Label: field.Label,
Content: req.FormValue(field.Name),
})
}
case "file":
{
file, fileHeader, err := req.FormFile(field.Name)
if err != nil {
if err == http.ErrMissingFile {
continue
}
return nil, nil, err
}
defer file.Close()

buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, file); err != nil {
return nil, nil, err
}
formFiles = append(formFiles, &mail.File{
Name: fileHeader.Filename,
MimeType: fileHeader.Header["Content-Type"][0],
Data: buf.Bytes(),
})
}
}
func (f FormattedField) GetFormattedString(req *http.Request) string {
var fields []any
for _, fieldName := range f.Fields {
fields = append(fields, req.FormValue(fieldName))
}

return formContent, formFiles, nil
}

func (f Form) GetReplyTo(req *http.Request) string {
if f.ReplyTo != nil {
var fields []any
for _, fieldName := range f.ReplyTo.Fields {
fields = append(fields, req.FormValue(fieldName))
}
return fmt.Sprintf(f.ReplyTo.Format, fields...)
}

return ""
}

func (f Form) GetSubject(req *http.Request) string {
if f.SubjectField != "" {
return req.FormValue(f.SubjectField)
}

return f.Subject
return fmt.Sprintf(f.Format, fields...)
}

func (f Form) HTML(content []*StringField) (string, error) {
tmpl, err := template.ParseFiles(f.Template)
tmpl, err := template.ParseFiles(f.MailConfig.Template)
if err != nil {
return "", err
}
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ require (
)

require (
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
github.com/emersion/go-webdav v0.5.0
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
Expand All @@ -34,10 +36,9 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/toorop/go-dkim v0.0.0-20240103092955-90b7d1423f92 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
Expand Down
Loading

0 comments on commit 07e0ecd

Please sign in to comment.