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.
 
 
 
 

215 lines
5.2 KiB

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