A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

237 lines
5.8 KiB

  1. package alpscarddav
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/url"
  6. "path"
  7. "strings"
  8. "git.sr.ht/~migadu/alps"
  9. "github.com/emersion/go-vcard"
  10. "github.com/emersion/go-webdav/carddav"
  11. "github.com/google/uuid"
  12. "github.com/labstack/echo/v4"
  13. )
  14. type AddressBookRenderData struct {
  15. alps.BaseRenderData
  16. AddressBook *carddav.AddressBook
  17. AddressObjects []AddressObject
  18. Query string
  19. }
  20. type AddressObjectRenderData struct {
  21. alps.BaseRenderData
  22. AddressBook *carddav.AddressBook
  23. AddressObject AddressObject
  24. }
  25. type UpdateAddressObjectRenderData struct {
  26. alps.BaseRenderData
  27. AddressBook *carddav.AddressBook
  28. AddressObject *carddav.AddressObject // nil if creating a new contact
  29. Card vcard.Card
  30. }
  31. func parseObjectPath(s string) (string, error) {
  32. p, err := url.PathUnescape(s)
  33. if err != nil {
  34. err = fmt.Errorf("failed to parse path: %v", err)
  35. return "", echo.NewHTTPError(http.StatusBadRequest, err)
  36. }
  37. return string(p), nil
  38. }
  39. func registerRoutes(p *plugin) {
  40. p.GET("/contacts", func(ctx *alps.Context) error {
  41. queryText := ctx.QueryParam("query")
  42. c, addressBook, err := p.clientWithAddressBook(ctx.Session)
  43. if err != nil {
  44. return err
  45. }
  46. query := carddav.AddressBookQuery{
  47. DataRequest: carddav.AddressDataRequest{
  48. Props: []string{
  49. vcard.FieldFormattedName,
  50. vcard.FieldEmail,
  51. vcard.FieldUID,
  52. },
  53. },
  54. PropFilters: []carddav.PropFilter{{
  55. Name: vcard.FieldFormattedName,
  56. }},
  57. }
  58. if queryText != "" {
  59. query.PropFilters = []carddav.PropFilter{
  60. {
  61. Name: vcard.FieldFormattedName,
  62. TextMatches: []carddav.TextMatch{{Text: queryText}},
  63. },
  64. {
  65. Name: vcard.FieldEmail,
  66. TextMatches: []carddav.TextMatch{{Text: queryText}},
  67. },
  68. }
  69. }
  70. aos, err := c.QueryAddressBook(addressBook.Path, &query)
  71. if err != nil {
  72. return fmt.Errorf("failed to query CardDAV addresses: %v", err)
  73. }
  74. return ctx.Render(http.StatusOK, "address-book.html", &AddressBookRenderData{
  75. BaseRenderData: *alps.NewBaseRenderData(ctx),
  76. AddressBook: addressBook,
  77. AddressObjects: newAddressObjectList(aos),
  78. Query: queryText,
  79. })
  80. })
  81. p.GET("/contacts/:path", func(ctx *alps.Context) error {
  82. path, err := parseObjectPath(ctx.Param("path"))
  83. if err != nil {
  84. return err
  85. }
  86. c, addressBook, err := p.clientWithAddressBook(ctx.Session)
  87. if err != nil {
  88. return err
  89. }
  90. multiGet := carddav.AddressBookMultiGet{
  91. DataRequest: carddav.AddressDataRequest{
  92. Props: []string{
  93. vcard.FieldFormattedName,
  94. vcard.FieldEmail,
  95. vcard.FieldUID,
  96. },
  97. },
  98. }
  99. aos, err := c.MultiGetAddressBook(path, &multiGet)
  100. if err != nil {
  101. return fmt.Errorf("failed to query CardDAV address: %v", err)
  102. }
  103. if len(aos) != 1 {
  104. return fmt.Errorf("expected exactly one address object with path %q, got %v", path, len(aos))
  105. }
  106. ao := &aos[0]
  107. return ctx.Render(http.StatusOK, "address-object.html", &AddressObjectRenderData{
  108. BaseRenderData: *alps.NewBaseRenderData(ctx),
  109. AddressBook: addressBook,
  110. AddressObject: AddressObject{ao},
  111. })
  112. })
  113. updateContact := func(ctx *alps.Context) error {
  114. addressObjectPath, err := parseObjectPath(ctx.Param("path"))
  115. if err != nil {
  116. return err
  117. }
  118. c, addressBook, err := p.clientWithAddressBook(ctx.Session)
  119. if err != nil {
  120. return err
  121. }
  122. var ao *carddav.AddressObject
  123. var card vcard.Card
  124. if addressObjectPath != "" {
  125. ao, err := c.GetAddressObject(addressObjectPath)
  126. if err != nil {
  127. return fmt.Errorf("failed to query CardDAV address: %v", err)
  128. }
  129. card = ao.Card
  130. } else {
  131. card = make(vcard.Card)
  132. }
  133. if ctx.Request().Method == "POST" {
  134. fn := ctx.FormValue("fn")
  135. emails := strings.Split(ctx.FormValue("emails"), ",")
  136. if _, ok := card[vcard.FieldVersion]; !ok {
  137. // Some CardDAV servers (e.g. Google) don't support vCard 4.0
  138. var version = "4.0"
  139. if !addressBook.SupportsAddressData(vcard.MIMEType, version) {
  140. version = "3.0"
  141. }
  142. if !addressBook.SupportsAddressData(vcard.MIMEType, version) {
  143. return fmt.Errorf("upstream CardDAV server doesn't support vCard %v", version)
  144. }
  145. card.SetValue(vcard.FieldVersion, version)
  146. }
  147. if field := card.Preferred(vcard.FieldFormattedName); field != nil {
  148. field.Value = fn
  149. } else {
  150. card.Add(vcard.FieldFormattedName, &vcard.Field{Value: fn})
  151. }
  152. // TODO: Google wants a "N" field, fails with a 400 otherwise
  153. // TODO: params are lost here
  154. var emailFields []*vcard.Field
  155. for _, email := range emails {
  156. emailFields = append(emailFields, &vcard.Field{
  157. Value: strings.TrimSpace(email),
  158. })
  159. }
  160. card[vcard.FieldEmail] = emailFields
  161. id := uuid.New()
  162. if _, ok := card[vcard.FieldUID]; !ok {
  163. card.SetValue(vcard.FieldUID, id.URN())
  164. }
  165. var p string
  166. if ao != nil {
  167. p = ao.Path
  168. } else {
  169. p = path.Join(addressBook.Path, id.String()+".vcf")
  170. }
  171. ao, err = c.PutAddressObject(p, card)
  172. if err != nil {
  173. return fmt.Errorf("failed to put address object: %v", err)
  174. }
  175. return ctx.Redirect(http.StatusFound, AddressObject{ao}.URL())
  176. }
  177. return ctx.Render(http.StatusOK, "update-address-object.html", &UpdateAddressObjectRenderData{
  178. BaseRenderData: *alps.NewBaseRenderData(ctx),
  179. AddressBook: addressBook,
  180. AddressObject: ao,
  181. Card: card,
  182. })
  183. }
  184. p.GET("/contacts/create", updateContact)
  185. p.POST("/contacts/create", updateContact)
  186. p.GET("/contacts/:path/edit", updateContact)
  187. p.POST("/contacts/:path/edit", updateContact)
  188. p.POST("/contacts/:path/delete", func(ctx *alps.Context) error {
  189. path, err := parseObjectPath(ctx.Param("path"))
  190. if err != nil {
  191. return err
  192. }
  193. c, err := p.client(ctx.Session)
  194. if err != nil {
  195. return err
  196. }
  197. if err := c.RemoveAll(path); err != nil {
  198. return fmt.Errorf("failed to delete address object: %v", err)
  199. }
  200. return ctx.Redirect(http.StatusFound, "/contacts")
  201. })
  202. }