A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 

1379 行
38 KiB

  1. /*
  2. * Copyright © 2018-2021 Musing Studio LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package writefreely
  11. import (
  12. "encoding/json"
  13. "fmt"
  14. "github.com/mailgun/mailgun-go"
  15. "html/template"
  16. "net/http"
  17. "regexp"
  18. "strconv"
  19. "strings"
  20. "sync"
  21. "time"
  22. "github.com/gorilla/csrf"
  23. "github.com/gorilla/mux"
  24. "github.com/gorilla/sessions"
  25. "github.com/guregu/null/zero"
  26. "github.com/writeas/impart"
  27. "github.com/writeas/web-core/auth"
  28. "github.com/writeas/web-core/data"
  29. "github.com/writeas/web-core/log"
  30. "github.com/writefreely/writefreely/author"
  31. "github.com/writefreely/writefreely/config"
  32. "github.com/writefreely/writefreely/page"
  33. )
  34. type (
  35. userSettings struct {
  36. Username string `schema:"username" json:"username"`
  37. Email string `schema:"email" json:"email"`
  38. NewPass string `schema:"new-pass" json:"new_pass"`
  39. OldPass string `schema:"current-pass" json:"current_pass"`
  40. IsLogOut bool `schema:"logout" json:"logout"`
  41. }
  42. UserPage struct {
  43. page.StaticPage
  44. PageTitle string
  45. Separator template.HTML
  46. IsAdmin bool
  47. CanInvite bool
  48. CollAlias string
  49. }
  50. )
  51. func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []string) *UserPage {
  52. up := &UserPage{
  53. StaticPage: pageForReq(app, r),
  54. PageTitle: title,
  55. }
  56. up.Username = u.Username
  57. up.Flashes = flashes
  58. up.Path = r.URL.Path
  59. up.IsAdmin = u.IsAdmin()
  60. up.CanInvite = canUserInvite(app.cfg, up.IsAdmin)
  61. return up
  62. }
  63. func canUserInvite(cfg *config.Config, isAdmin bool) bool {
  64. return cfg.App.UserInvites != "" &&
  65. (isAdmin || cfg.App.UserInvites != "admin")
  66. }
  67. func (up *UserPage) SetMessaging(u *User) {
  68. // up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID)
  69. }
  70. const (
  71. loginAttemptExpiration = 3 * time.Second
  72. )
  73. var actuallyUsernameReg = regexp.MustCompile("username is actually ([a-z0-9\\-]+)\\. Please try that, instead")
  74. func apiSignup(app *App, w http.ResponseWriter, r *http.Request) error {
  75. _, err := signup(app, w, r)
  76. return err
  77. }
  78. func signup(app *App, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
  79. if app.cfg.App.DisablePasswordAuth {
  80. err := ErrDisabledPasswordAuth
  81. return nil, err
  82. }
  83. reqJSON := IsJSON(r)
  84. // Get params
  85. var ur userRegistration
  86. if reqJSON {
  87. decoder := json.NewDecoder(r.Body)
  88. err := decoder.Decode(&ur)
  89. if err != nil {
  90. log.Error("Couldn't parse signup JSON request: %v\n", err)
  91. return nil, ErrBadJSON
  92. }
  93. } else {
  94. // Check if user is already logged in
  95. u := getUserSession(app, r)
  96. if u != nil {
  97. return &AuthUser{User: u}, nil
  98. }
  99. err := r.ParseForm()
  100. if err != nil {
  101. log.Error("Couldn't parse signup form request: %v\n", err)
  102. return nil, ErrBadFormData
  103. }
  104. err = app.formDecoder.Decode(&ur, r.PostForm)
  105. if err != nil {
  106. log.Error("Couldn't decode signup form request: %v\n", err)
  107. return nil, ErrBadFormData
  108. }
  109. }
  110. return signupWithRegistration(app, ur, w, r)
  111. }
  112. func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWriter, r *http.Request) (*AuthUser, error) {
  113. reqJSON := IsJSON(r)
  114. // Validate required params (alias)
  115. if signup.Alias == "" {
  116. return nil, impart.HTTPError{http.StatusBadRequest, "A username is required."}
  117. }
  118. if signup.Pass == "" {
  119. return nil, impart.HTTPError{http.StatusBadRequest, "A password is required."}
  120. }
  121. var desiredUsername string
  122. if signup.Normalize {
  123. // With this option we simply conform the username to what we expect
  124. // without complaining. Since they might've done something funny, like
  125. // enter: write.as/Way Out There, we'll use their raw input for the new
  126. // collection name and sanitize for the slug / username.
  127. desiredUsername = signup.Alias
  128. signup.Alias = getSlug(signup.Alias, "")
  129. }
  130. if !author.IsValidUsername(app.cfg, signup.Alias) {
  131. // Ensure the username is syntactically correct.
  132. return nil, impart.HTTPError{http.StatusPreconditionFailed, "Username is reserved or isn't valid. It must be at least 3 characters long, and can only include letters, numbers, and hyphens."}
  133. }
  134. // Handle empty optional params
  135. hashedPass, err := auth.HashPass([]byte(signup.Pass))
  136. if err != nil {
  137. return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
  138. }
  139. // Create struct to insert
  140. u := &User{
  141. Username: signup.Alias,
  142. HashedPass: hashedPass,
  143. HasPass: true,
  144. Email: prepareUserEmail(signup.Email, app.keys.EmailKey),
  145. Created: time.Now().Truncate(time.Second).UTC(),
  146. }
  147. // Create actual user
  148. if err := app.db.CreateUser(app.cfg, u, desiredUsername, signup.Description); err != nil {
  149. return nil, err
  150. }
  151. // Log invite if needed
  152. if signup.InviteCode != "" {
  153. err = app.db.CreateInvitedUser(signup.InviteCode, u.ID)
  154. if err != nil {
  155. return nil, err
  156. }
  157. }
  158. // Add back unencrypted data for response
  159. if signup.Email != "" {
  160. u.Email.String = signup.Email
  161. }
  162. resUser := &AuthUser{
  163. User: u,
  164. }
  165. title := signup.Alias
  166. if signup.Normalize {
  167. title = desiredUsername
  168. }
  169. resUser.Collections = &[]Collection{
  170. {
  171. Alias: signup.Alias,
  172. Title: title,
  173. Description: signup.Description,
  174. },
  175. }
  176. var coll *Collection
  177. if signup.Monetization != "" {
  178. if coll == nil {
  179. coll, err = app.db.GetCollection(signup.Alias)
  180. if err != nil {
  181. log.Error("Unable to get new collection '%s' for monetization on signup: %v", signup.Alias, err)
  182. return nil, err
  183. }
  184. }
  185. err = app.db.SetCollectionAttribute(coll.ID, "monetization_pointer", signup.Monetization)
  186. if err != nil {
  187. log.Error("Unable to add monetization on signup: %v", err)
  188. return nil, err
  189. }
  190. coll.Monetization = signup.Monetization
  191. }
  192. var token string
  193. if reqJSON && !signup.Web {
  194. token, err = app.db.GetAccessToken(u.ID)
  195. if err != nil {
  196. return nil, impart.HTTPError{http.StatusInternalServerError, "Could not create access token. Try re-authenticating."}
  197. }
  198. resUser.AccessToken = token
  199. } else {
  200. session, err := app.sessionStore.Get(r, cookieName)
  201. if err != nil {
  202. // The cookie should still save, even if there's an error.
  203. // Source: https://github.com/gorilla/sessions/issues/16#issuecomment-143642144
  204. log.Error("Session: %v; ignoring", err)
  205. }
  206. session.Values[cookieUserVal] = resUser.User.Cookie()
  207. err = session.Save(r, w)
  208. if err != nil {
  209. log.Error("Couldn't save session: %v", err)
  210. return nil, err
  211. }
  212. }
  213. if reqJSON {
  214. return resUser, impart.WriteSuccess(w, resUser, http.StatusCreated)
  215. }
  216. return resUser, nil
  217. }
  218. func viewLogout(app *App, w http.ResponseWriter, r *http.Request) error {
  219. session, err := app.sessionStore.Get(r, cookieName)
  220. if err != nil {
  221. return ErrInternalCookieSession
  222. }
  223. // Ensure user has an email or password set before they go, so they don't
  224. // lose access to their account.
  225. val := session.Values[cookieUserVal]
  226. var u = &User{}
  227. var ok bool
  228. if u, ok = val.(*User); !ok {
  229. log.Error("Error casting user object on logout. Vals: %+v Resetting cookie.", session.Values)
  230. err = session.Save(r, w)
  231. if err != nil {
  232. log.Error("Couldn't save session on logout: %v", err)
  233. return impart.HTTPError{http.StatusInternalServerError, "Unable to save cookie session."}
  234. }
  235. return impart.HTTPError{http.StatusFound, "/"}
  236. }
  237. u, err = app.db.GetUserByID(u.ID)
  238. if err != nil && err != ErrUserNotFound {
  239. return impart.HTTPError{http.StatusInternalServerError, "Unable to fetch user information."}
  240. }
  241. session.Options.MaxAge = -1
  242. err = session.Save(r, w)
  243. if err != nil {
  244. log.Error("Couldn't save session on logout: %v", err)
  245. return impart.HTTPError{http.StatusInternalServerError, "Unable to save cookie session."}
  246. }
  247. return impart.HTTPError{http.StatusFound, "/"}
  248. }
  249. func handleAPILogout(app *App, w http.ResponseWriter, r *http.Request) error {
  250. accessToken := r.Header.Get("Authorization")
  251. if accessToken == "" {
  252. return ErrNoAccessToken
  253. }
  254. t := auth.GetToken(accessToken)
  255. if len(t) == 0 {
  256. return ErrNoAccessToken
  257. }
  258. err := app.db.DeleteToken(t)
  259. if err != nil {
  260. return err
  261. }
  262. return impart.HTTPError{Status: http.StatusNoContent}
  263. }
  264. func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
  265. var earlyError string
  266. oneTimeToken := r.FormValue("with")
  267. if oneTimeToken != "" {
  268. log.Info("Calling login with one-time token.")
  269. err := login(app, w, r)
  270. if err != nil {
  271. log.Info("Received error: %v", err)
  272. earlyError = fmt.Sprintf("%s", err)
  273. }
  274. }
  275. session, err := app.sessionStore.Get(r, cookieName)
  276. if err != nil {
  277. // Ignore this
  278. log.Error("Unable to get session; ignoring: %v", err)
  279. }
  280. p := &struct {
  281. page.StaticPage
  282. *OAuthButtons
  283. To string
  284. Message template.HTML
  285. Flashes []template.HTML
  286. LoginUsername string
  287. }{
  288. StaticPage: pageForReq(app, r),
  289. OAuthButtons: NewOAuthButtons(app.Config()),
  290. To: r.FormValue("to"),
  291. Message: template.HTML(""),
  292. Flashes: []template.HTML{},
  293. LoginUsername: getTempInfo(app, "login-user", r, w),
  294. }
  295. if earlyError != "" {
  296. p.Flashes = append(p.Flashes, template.HTML(earlyError))
  297. }
  298. // Display any error messages
  299. flashes, _ := getSessionFlashes(app, w, r, session)
  300. for _, flash := range flashes {
  301. p.Flashes = append(p.Flashes, template.HTML(flash))
  302. }
  303. err = pages["login.tmpl"].ExecuteTemplate(w, "base", p)
  304. if err != nil {
  305. log.Error("Unable to render login: %v", err)
  306. return err
  307. }
  308. return nil
  309. }
  310. func webLogin(app *App, w http.ResponseWriter, r *http.Request) error {
  311. err := login(app, w, r)
  312. if err != nil {
  313. username := r.FormValue("alias")
  314. // Login request was unsuccessful; save the error in the session and redirect them
  315. if err, ok := err.(impart.HTTPError); ok {
  316. session, _ := app.sessionStore.Get(r, cookieName)
  317. if session != nil {
  318. session.AddFlash(err.Message)
  319. session.Save(r, w)
  320. }
  321. if m := actuallyUsernameReg.FindStringSubmatch(err.Message); len(m) > 0 {
  322. // Retain fixed username recommendation for the login form
  323. username = m[1]
  324. }
  325. }
  326. // Pass along certain information
  327. saveTempInfo(app, "login-user", username, r, w)
  328. // Retain post-login URL if one was given
  329. redirectTo := "/login"
  330. postLoginRedirect := r.FormValue("to")
  331. if postLoginRedirect != "" {
  332. redirectTo += "?to=" + postLoginRedirect
  333. }
  334. log.Error("Unable to login: %v", err)
  335. return impart.HTTPError{http.StatusTemporaryRedirect, redirectTo}
  336. }
  337. return nil
  338. }
  339. var loginAttemptUsers = sync.Map{}
  340. func login(app *App, w http.ResponseWriter, r *http.Request) error {
  341. reqJSON := IsJSON(r)
  342. oneTimeToken := r.FormValue("with")
  343. verbose := r.FormValue("all") == "true" || r.FormValue("verbose") == "1" || r.FormValue("verbose") == "true" || (reqJSON && oneTimeToken != "")
  344. redirectTo := r.FormValue("to")
  345. if redirectTo == "" {
  346. if app.cfg.App.SingleUser {
  347. redirectTo = "/me/new"
  348. } else {
  349. redirectTo = "/"
  350. }
  351. }
  352. var u *User
  353. var err error
  354. var signin userCredentials
  355. if app.cfg.App.DisablePasswordAuth {
  356. err := ErrDisabledPasswordAuth
  357. return err
  358. }
  359. // Log in with one-time token if one is given
  360. if oneTimeToken != "" {
  361. log.Info("Login: Logging user in via token.")
  362. userID := app.db.GetUserID(oneTimeToken)
  363. if userID == -1 {
  364. log.Error("Login: Got user -1 from token")
  365. err := ErrBadAccessToken
  366. err.Message = "Expired or invalid login code."
  367. return err
  368. }
  369. log.Info("Login: Found user %d.", userID)
  370. u, err = app.db.GetUserByID(userID)
  371. if err != nil {
  372. log.Error("Unable to fetch user on one-time token login: %v", err)
  373. return impart.HTTPError{http.StatusInternalServerError, "There was an error retrieving the user you want."}
  374. }
  375. log.Info("Login: Got user via token")
  376. } else {
  377. // Get params
  378. if reqJSON {
  379. decoder := json.NewDecoder(r.Body)
  380. err := decoder.Decode(&signin)
  381. if err != nil {
  382. log.Error("Couldn't parse signin JSON request: %v\n", err)
  383. return ErrBadJSON
  384. }
  385. } else {
  386. err := r.ParseForm()
  387. if err != nil {
  388. log.Error("Couldn't parse signin form request: %v\n", err)
  389. return ErrBadFormData
  390. }
  391. err = app.formDecoder.Decode(&signin, r.PostForm)
  392. if err != nil {
  393. log.Error("Couldn't decode signin form request: %v\n", err)
  394. return ErrBadFormData
  395. }
  396. }
  397. log.Info("Login: Attempting login for '%s'", signin.Alias)
  398. // Validate required params (all)
  399. if signin.Alias == "" {
  400. msg := "Parameter `alias` required."
  401. if signin.Web {
  402. msg = "A username is required."
  403. }
  404. return impart.HTTPError{http.StatusBadRequest, msg}
  405. }
  406. if !signin.EmailLogin && signin.Pass == "" {
  407. msg := "Parameter `pass` required."
  408. if signin.Web {
  409. msg = "A password is required."
  410. }
  411. return impart.HTTPError{http.StatusBadRequest, msg}
  412. }
  413. // Prevent excessive login attempts on the same account
  414. // Skip this check in dev environment
  415. if !app.cfg.Server.Dev {
  416. now := time.Now()
  417. attemptExp, att := loginAttemptUsers.LoadOrStore(signin.Alias, now.Add(loginAttemptExpiration))
  418. if att {
  419. if attemptExpTime, ok := attemptExp.(time.Time); ok {
  420. if attemptExpTime.After(now) {
  421. // This user attempted previously, and the period hasn't expired yet
  422. return impart.HTTPError{http.StatusTooManyRequests, "You're doing that too much."}
  423. } else {
  424. // This user attempted previously, but the time expired; free up space
  425. loginAttemptUsers.Delete(signin.Alias)
  426. }
  427. } else {
  428. log.Error("Unable to cast expiration to time")
  429. }
  430. }
  431. }
  432. // Retrieve password
  433. u, err = app.db.GetUserForAuth(signin.Alias)
  434. if err != nil {
  435. log.Info("Unable to getUserForAuth on %s: %v", signin.Alias, err)
  436. if strings.IndexAny(signin.Alias, "@") > 0 {
  437. log.Info("Suggesting: %s", ErrUserNotFoundEmail.Message)
  438. return ErrUserNotFoundEmail
  439. }
  440. return err
  441. }
  442. // Authenticate
  443. if u.Email.String == "" {
  444. // User has no email set, so check if they haven't added a password, either,
  445. // so we can return a more helpful error message.
  446. if hasPass, _ := app.db.IsUserPassSet(u.ID); !hasPass {
  447. log.Info("Tried logging into %s, but no password or email.", signin.Alias)
  448. return impart.HTTPError{http.StatusPreconditionFailed, "This user never added a password or email address. Please contact us for help."}
  449. }
  450. }
  451. if len(u.HashedPass) == 0 {
  452. return impart.HTTPError{http.StatusUnauthorized, "This user never set a password. Perhaps try logging in via OAuth?"}
  453. }
  454. if !auth.Authenticated(u.HashedPass, []byte(signin.Pass)) {
  455. return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."}
  456. }
  457. }
  458. if reqJSON && !signin.Web {
  459. var token string
  460. if r.Header.Get("User-Agent") == "" {
  461. // Get last created token when User-Agent is empty
  462. token = app.db.FetchLastAccessToken(u.ID)
  463. if token == "" {
  464. token, err = app.db.GetAccessToken(u.ID)
  465. }
  466. } else {
  467. token, err = app.db.GetAccessToken(u.ID)
  468. }
  469. if err != nil {
  470. log.Error("Login: Unable to create access token: %v", err)
  471. return impart.HTTPError{http.StatusInternalServerError, "Could not create access token. Try re-authenticating."}
  472. }
  473. resUser := getVerboseAuthUser(app, token, u, verbose)
  474. return impart.WriteSuccess(w, resUser, http.StatusOK)
  475. }
  476. session, err := app.sessionStore.Get(r, cookieName)
  477. if err != nil {
  478. // The cookie should still save, even if there's an error.
  479. log.Error("Login: Session: %v; ignoring", err)
  480. }
  481. // Remove unwanted data
  482. session.Values[cookieUserVal] = u.Cookie()
  483. err = session.Save(r, w)
  484. if err != nil {
  485. log.Error("Login: Couldn't save session: %v", err)
  486. // TODO: return error
  487. }
  488. // Send success
  489. if reqJSON {
  490. return impart.WriteSuccess(w, &AuthUser{User: u}, http.StatusOK)
  491. }
  492. log.Info("Login: Redirecting to %s", redirectTo)
  493. w.Header().Set("Location", redirectTo)
  494. w.WriteHeader(http.StatusFound)
  495. return nil
  496. }
  497. func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser {
  498. resUser := &AuthUser{
  499. AccessToken: token,
  500. User: u,
  501. }
  502. // Fetch verbose user data if requested
  503. if verbose {
  504. posts, err := app.db.GetUserPosts(u)
  505. if err != nil {
  506. log.Error("Login: Unable to get user posts: %v", err)
  507. }
  508. colls, err := app.db.GetCollections(u, app.cfg.App.Host)
  509. if err != nil {
  510. log.Error("Login: Unable to get user collections: %v", err)
  511. }
  512. passIsSet, err := app.db.IsUserPassSet(u.ID)
  513. if err != nil {
  514. // TODO: correct error message
  515. log.Error("Login: Unable to get user collections: %v", err)
  516. }
  517. resUser.Posts = posts
  518. resUser.Collections = colls
  519. resUser.User.HasPass = passIsSet
  520. }
  521. return resUser
  522. }
  523. func viewExportOptions(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  524. // Fetch extra user data
  525. p := NewUserPage(app, r, u, "Export", nil)
  526. showUserPage(w, "export", p)
  527. return nil
  528. }
  529. func viewExportPosts(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
  530. var filename string
  531. var u = &User{}
  532. reqJSON := IsJSON(r)
  533. if reqJSON {
  534. // Use given Authorization header
  535. accessToken := r.Header.Get("Authorization")
  536. if accessToken == "" {
  537. return nil, filename, ErrNoAccessToken
  538. }
  539. userID := app.db.GetUserID(accessToken)
  540. if userID == -1 {
  541. return nil, filename, ErrBadAccessToken
  542. }
  543. var err error
  544. u, err = app.db.GetUserByID(userID)
  545. if err != nil {
  546. return nil, filename, impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve requested user."}
  547. }
  548. } else {
  549. // Use user cookie
  550. session, err := app.sessionStore.Get(r, cookieName)
  551. if err != nil {
  552. // The cookie should still save, even if there's an error.
  553. log.Error("Session: %v; ignoring", err)
  554. }
  555. val := session.Values[cookieUserVal]
  556. var ok bool
  557. if u, ok = val.(*User); !ok {
  558. return nil, filename, ErrNotLoggedIn
  559. }
  560. }
  561. filename = u.Username + "-posts-" + time.Now().Truncate(time.Second).UTC().Format("200601021504")
  562. // Fetch data we're exporting
  563. var err error
  564. var data []byte
  565. posts, err := app.db.GetUserPosts(u)
  566. if err != nil {
  567. return data, filename, err
  568. }
  569. // Export as CSV
  570. if strings.HasSuffix(r.URL.Path, ".csv") {
  571. data = exportPostsCSV(app.cfg.App.Host, u, posts)
  572. return data, filename, err
  573. }
  574. if strings.HasSuffix(r.URL.Path, ".zip") {
  575. data = exportPostsZip(u, posts)
  576. return data, filename, err
  577. }
  578. if r.FormValue("pretty") == "1" {
  579. data, err = json.MarshalIndent(posts, "", "\t")
  580. } else {
  581. data, err = json.Marshal(posts)
  582. }
  583. return data, filename, err
  584. }
  585. func viewExportFull(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) {
  586. var err error
  587. filename := ""
  588. u := getUserSession(app, r)
  589. if u == nil {
  590. return nil, filename, ErrNotLoggedIn
  591. }
  592. filename = u.Username + "-" + time.Now().Truncate(time.Second).UTC().Format("200601021504")
  593. exportUser := compileFullExport(app, u)
  594. var data []byte
  595. if r.FormValue("pretty") == "1" {
  596. data, err = json.MarshalIndent(exportUser, "", "\t")
  597. } else {
  598. data, err = json.Marshal(exportUser)
  599. }
  600. return data, filename, err
  601. }
  602. func viewMeAPI(app *App, w http.ResponseWriter, r *http.Request) error {
  603. reqJSON := IsJSON(r)
  604. uObj := struct {
  605. ID int64 `json:"id,omitempty"`
  606. Username string `json:"username,omitempty"`
  607. }{}
  608. var err error
  609. if reqJSON {
  610. _, uObj.Username, err = app.db.GetUserDataFromToken(r.Header.Get("Authorization"))
  611. if err != nil {
  612. return err
  613. }
  614. } else {
  615. u := getUserSession(app, r)
  616. if u == nil {
  617. return impart.WriteSuccess(w, uObj, http.StatusOK)
  618. }
  619. uObj.Username = u.Username
  620. }
  621. return impart.WriteSuccess(w, uObj, http.StatusOK)
  622. }
  623. func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  624. reqJSON := IsJSON(r)
  625. if !reqJSON {
  626. return ErrBadRequestedType
  627. }
  628. isAnonPosts := r.FormValue("anonymous") == "1"
  629. if isAnonPosts {
  630. pageStr := r.FormValue("page")
  631. pg, err := strconv.Atoi(pageStr)
  632. if err != nil {
  633. log.Error("Error parsing page parameter '%s': %s", pageStr, err)
  634. pg = 1
  635. }
  636. p, err := app.db.GetAnonymousPosts(u, pg)
  637. if err != nil {
  638. return err
  639. }
  640. return impart.WriteSuccess(w, p, http.StatusOK)
  641. }
  642. var err error
  643. p := GetPostsCache(u.ID)
  644. if p == nil {
  645. userPostsCache.Lock()
  646. if userPostsCache.users[u.ID].ready == nil {
  647. userPostsCache.users[u.ID] = postsCacheItem{ready: make(chan struct{})}
  648. userPostsCache.Unlock()
  649. p, err = app.db.GetUserPosts(u)
  650. if err != nil {
  651. return err
  652. }
  653. CachePosts(u.ID, p)
  654. } else {
  655. userPostsCache.Unlock()
  656. <-userPostsCache.users[u.ID].ready
  657. p = GetPostsCache(u.ID)
  658. }
  659. }
  660. return impart.WriteSuccess(w, p, http.StatusOK)
  661. }
  662. func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  663. reqJSON := IsJSON(r)
  664. if !reqJSON {
  665. return ErrBadRequestedType
  666. }
  667. p, err := app.db.GetCollections(u, app.cfg.App.Host)
  668. if err != nil {
  669. return err
  670. }
  671. return impart.WriteSuccess(w, p, http.StatusOK)
  672. }
  673. func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  674. p, err := app.db.GetAnonymousPosts(u, 1)
  675. if err != nil {
  676. log.Error("unable to fetch anon posts: %v", err)
  677. }
  678. // nil-out AnonymousPosts slice for easy detection in the template
  679. if p != nil && len(*p) == 0 {
  680. p = nil
  681. }
  682. f, err := getSessionFlashes(app, w, r, nil)
  683. if err != nil {
  684. log.Error("unable to fetch flashes: %v", err)
  685. }
  686. c, err := app.db.GetPublishableCollections(u, app.cfg.App.Host)
  687. if err != nil {
  688. log.Error("unable to fetch collections: %v", err)
  689. }
  690. silenced, err := app.db.IsUserSilenced(u.ID)
  691. if err != nil {
  692. if err == ErrUserNotFound {
  693. return err
  694. }
  695. log.Error("view articles: %v", err)
  696. }
  697. d := struct {
  698. *UserPage
  699. AnonymousPosts *[]PublicPost
  700. Collections *[]Collection
  701. Silenced bool
  702. }{
  703. UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f),
  704. AnonymousPosts: p,
  705. Collections: c,
  706. Silenced: silenced,
  707. }
  708. d.UserPage.SetMessaging(u)
  709. w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
  710. w.Header().Set("Expires", "Thu, 04 Oct 1990 20:00:00 GMT")
  711. showUserPage(w, "articles", d)
  712. return nil
  713. }
  714. func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  715. c, err := app.db.GetCollections(u, app.cfg.App.Host)
  716. if err != nil {
  717. log.Error("unable to fetch collections: %v", err)
  718. return fmt.Errorf("No collections")
  719. }
  720. f, _ := getSessionFlashes(app, w, r, nil)
  721. uc, _ := app.db.GetUserCollectionCount(u.ID)
  722. // TODO: handle any errors
  723. silenced, err := app.db.IsUserSilenced(u.ID)
  724. if err != nil {
  725. if err == ErrUserNotFound {
  726. return err
  727. }
  728. log.Error("view collections: %v", err)
  729. return fmt.Errorf("view collections: %v", err)
  730. }
  731. d := struct {
  732. *UserPage
  733. Collections *[]Collection
  734. UsedCollections, TotalCollections int
  735. NewBlogsDisabled bool
  736. Silenced bool
  737. }{
  738. UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f),
  739. Collections: c,
  740. UsedCollections: int(uc),
  741. NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc),
  742. Silenced: silenced,
  743. }
  744. d.UserPage.SetMessaging(u)
  745. showUserPage(w, "collections", d)
  746. return nil
  747. }
  748. func viewEditCollection(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  749. vars := mux.Vars(r)
  750. c, err := app.db.GetCollection(vars["collection"])
  751. if err != nil {
  752. return err
  753. }
  754. if c.OwnerID != u.ID {
  755. return ErrCollectionNotFound
  756. }
  757. silenced, err := app.db.IsUserSilenced(u.ID)
  758. if err != nil {
  759. if err == ErrUserNotFound {
  760. return err
  761. }
  762. log.Error("view edit collection %v", err)
  763. return fmt.Errorf("view edit collection: %v", err)
  764. }
  765. flashes, _ := getSessionFlashes(app, w, r, nil)
  766. obj := struct {
  767. *UserPage
  768. *Collection
  769. Silenced bool
  770. config.EmailCfg
  771. LetterReplyTo string
  772. }{
  773. UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes),
  774. Collection: c,
  775. Silenced: silenced,
  776. EmailCfg: app.cfg.Email,
  777. }
  778. obj.UserPage.CollAlias = c.Alias
  779. if obj.EmailCfg.Enabled() {
  780. obj.LetterReplyTo = app.db.GetCollectionAttribute(c.ID, collAttrLetterReplyTo)
  781. }
  782. showUserPage(w, "collection", obj)
  783. return nil
  784. }
  785. func updateSettings(app *App, w http.ResponseWriter, r *http.Request) error {
  786. reqJSON := IsJSON(r)
  787. var s userSettings
  788. var u *User
  789. var sess *sessions.Session
  790. var err error
  791. if reqJSON {
  792. accessToken := r.Header.Get("Authorization")
  793. if accessToken == "" {
  794. return ErrNoAccessToken
  795. }
  796. u, err = app.db.GetAPIUser(accessToken)
  797. if err != nil {
  798. return ErrBadAccessToken
  799. }
  800. decoder := json.NewDecoder(r.Body)
  801. err := decoder.Decode(&s)
  802. if err != nil {
  803. log.Error("Couldn't parse settings JSON request: %v\n", err)
  804. return ErrBadJSON
  805. }
  806. // Prevent all username updates
  807. // TODO: support changing username via JSON API request
  808. s.Username = ""
  809. } else {
  810. u, sess = getUserAndSession(app, r)
  811. if u == nil {
  812. return ErrNotLoggedIn
  813. }
  814. err := r.ParseForm()
  815. if err != nil {
  816. log.Error("Couldn't parse settings form request: %v\n", err)
  817. return ErrBadFormData
  818. }
  819. err = app.formDecoder.Decode(&s, r.PostForm)
  820. if err != nil {
  821. log.Error("Couldn't decode settings form request: %v\n", err)
  822. return ErrBadFormData
  823. }
  824. }
  825. // Do update
  826. postUpdateReturn := r.FormValue("return")
  827. redirectTo := "/me/settings"
  828. if s.IsLogOut {
  829. redirectTo += "?logout=1"
  830. } else if postUpdateReturn != "" {
  831. redirectTo = postUpdateReturn
  832. }
  833. // Only do updates on values we need
  834. if s.Username != "" && s.Username == u.Username {
  835. // Username hasn't actually changed; blank it out
  836. s.Username = ""
  837. }
  838. err = app.db.ChangeSettings(app, u, &s)
  839. if err != nil {
  840. if reqJSON {
  841. return err
  842. }
  843. if err, ok := err.(impart.HTTPError); ok {
  844. addSessionFlash(app, w, r, err.Message, nil)
  845. }
  846. } else {
  847. // Successful update.
  848. if reqJSON {
  849. return impart.WriteSuccess(w, u, http.StatusOK)
  850. }
  851. if s.IsLogOut {
  852. redirectTo = "/me/logout"
  853. } else {
  854. sess.Values[cookieUserVal] = u.Cookie()
  855. addSessionFlash(app, w, r, "Account updated.", nil)
  856. }
  857. }
  858. w.Header().Set("Location", redirectTo)
  859. w.WriteHeader(http.StatusFound)
  860. return nil
  861. }
  862. func updatePassphrase(app *App, w http.ResponseWriter, r *http.Request) error {
  863. accessToken := r.Header.Get("Authorization")
  864. if accessToken == "" {
  865. return ErrNoAccessToken
  866. }
  867. curPass := r.FormValue("current")
  868. newPass := r.FormValue("new")
  869. // Ensure a new password is given (always required)
  870. if newPass == "" {
  871. return impart.HTTPError{http.StatusBadRequest, "Provide a new password."}
  872. }
  873. userID, sudo := app.db.GetUserIDPrivilege(accessToken)
  874. if userID == -1 {
  875. return ErrBadAccessToken
  876. }
  877. // Ensure a current password is given if the access token doesn't have sudo
  878. // privileges.
  879. if !sudo && curPass == "" {
  880. return impart.HTTPError{http.StatusBadRequest, "Provide current password."}
  881. }
  882. // Hash the new password
  883. hashedPass, err := auth.HashPass([]byte(newPass))
  884. if err != nil {
  885. return impart.HTTPError{http.StatusInternalServerError, "Could not create password hash."}
  886. }
  887. // Do update
  888. err = app.db.ChangePassphrase(userID, sudo, curPass, hashedPass)
  889. if err != nil {
  890. return err
  891. }
  892. return impart.WriteSuccess(w, struct{}{}, http.StatusOK)
  893. }
  894. func viewStats(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  895. var c *Collection
  896. var err error
  897. vars := mux.Vars(r)
  898. alias := vars["collection"]
  899. if alias != "" {
  900. c, err = app.db.GetCollection(alias)
  901. if err != nil {
  902. return err
  903. }
  904. if c.OwnerID != u.ID {
  905. return ErrCollectionNotFound
  906. }
  907. c.hostName = app.cfg.App.Host
  908. }
  909. topPosts, err := app.db.GetTopPosts(u, alias, c.hostName)
  910. if err != nil {
  911. log.Error("Unable to get top posts: %v", err)
  912. return err
  913. }
  914. flashes, _ := getSessionFlashes(app, w, r, nil)
  915. titleStats := ""
  916. if c != nil {
  917. titleStats = c.DisplayTitle() + " "
  918. }
  919. silenced, err := app.db.IsUserSilenced(u.ID)
  920. if err != nil {
  921. if err == ErrUserNotFound {
  922. return err
  923. }
  924. log.Error("view stats: %v", err)
  925. return err
  926. }
  927. obj := struct {
  928. *UserPage
  929. VisitsBlog string
  930. Collection *Collection
  931. TopPosts *[]PublicPost
  932. APFollowers int
  933. EmailEnabled bool
  934. EmailSubscribers int
  935. Silenced bool
  936. }{
  937. UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes),
  938. VisitsBlog: alias,
  939. Collection: c,
  940. TopPosts: topPosts,
  941. EmailEnabled: app.cfg.Email.Enabled(),
  942. Silenced: silenced,
  943. }
  944. obj.UserPage.CollAlias = c.Alias
  945. if app.cfg.App.Federation {
  946. folls, err := app.db.GetAPFollowers(c)
  947. if err != nil {
  948. return err
  949. }
  950. obj.APFollowers = len(*folls)
  951. }
  952. if obj.EmailEnabled {
  953. subs, err := app.db.GetEmailSubscribers(c.ID, true)
  954. if err != nil {
  955. return err
  956. }
  957. obj.EmailSubscribers = len(subs)
  958. }
  959. showUserPage(w, "stats", obj)
  960. return nil
  961. }
  962. func handleViewSubscribers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  963. vars := mux.Vars(r)
  964. c, err := app.db.GetCollection(vars["collection"])
  965. if err != nil {
  966. return err
  967. }
  968. filter := r.FormValue("filter")
  969. flashes, _ := getSessionFlashes(app, w, r, nil)
  970. obj := struct {
  971. *UserPage
  972. Collection CollectionNav
  973. EmailSubs []*EmailSubscriber
  974. Followers *[]RemoteUser
  975. Silenced bool
  976. Filter string
  977. FederationEnabled bool
  978. CanEmailSub bool
  979. CanAddSubs bool
  980. EmailSubsEnabled bool
  981. }{
  982. UserPage: NewUserPage(app, r, u, c.DisplayTitle()+" Subscribers", flashes),
  983. Collection: CollectionNav{
  984. Collection: c,
  985. Path: r.URL.Path,
  986. SingleUser: app.cfg.App.SingleUser,
  987. },
  988. Silenced: u.IsSilenced(),
  989. Filter: filter,
  990. FederationEnabled: app.cfg.App.Federation,
  991. CanEmailSub: app.cfg.Email.Enabled(),
  992. EmailSubsEnabled: c.EmailSubsEnabled(),
  993. }
  994. obj.Followers, err = app.db.GetAPFollowers(c)
  995. if err != nil {
  996. return err
  997. }
  998. obj.EmailSubs, err = app.db.GetEmailSubscribers(c.ID, true)
  999. if err != nil {
  1000. return err
  1001. }
  1002. if obj.Filter == "" {
  1003. // Set permission to add email subscribers
  1004. //obj.CanAddSubs = app.db.GetUserAttribute(c.OwnerID, userAttrCanAddEmailSubs) == "1"
  1005. }
  1006. showUserPage(w, "subscribers", obj)
  1007. return nil
  1008. }
  1009. func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  1010. fullUser, err := app.db.GetUserByID(u.ID)
  1011. if err != nil {
  1012. if err == ErrUserNotFound {
  1013. return err
  1014. }
  1015. log.Error("Unable to get user for settings: %s", err)
  1016. return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
  1017. }
  1018. passIsSet, err := app.db.IsUserPassSet(u.ID)
  1019. if err != nil {
  1020. log.Error("Unable to get isUserPassSet for settings: %s", err)
  1021. return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
  1022. }
  1023. flashes, _ := getSessionFlashes(app, w, r, nil)
  1024. enableOauthSlack := app.Config().SlackOauth.ClientID != ""
  1025. enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != ""
  1026. enableOauthGitLab := app.Config().GitlabOauth.ClientID != ""
  1027. enableOauthGeneric := app.Config().GenericOauth.ClientID != ""
  1028. enableOauthGitea := app.Config().GiteaOauth.ClientID != ""
  1029. oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID)
  1030. if err != nil {
  1031. log.Error("Unable to get oauth accounts for settings: %s", err)
  1032. return impart.HTTPError{http.StatusInternalServerError, "Unable to retrieve user data. The humans have been alerted."}
  1033. }
  1034. for idx, oauthAccount := range oauthAccounts {
  1035. switch oauthAccount.Provider {
  1036. case "slack":
  1037. enableOauthSlack = false
  1038. case "write.as":
  1039. enableOauthWriteAs = false
  1040. case "gitlab":
  1041. enableOauthGitLab = false
  1042. case "generic":
  1043. oauthAccounts[idx].DisplayName = app.Config().GenericOauth.DisplayName
  1044. oauthAccounts[idx].AllowDisconnect = app.Config().GenericOauth.AllowDisconnect
  1045. enableOauthGeneric = false
  1046. case "gitea":
  1047. enableOauthGitea = false
  1048. }
  1049. }
  1050. displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGeneric || enableOauthGitea || len(oauthAccounts) > 0
  1051. obj := struct {
  1052. *UserPage
  1053. Email string
  1054. HasPass bool
  1055. IsLogOut bool
  1056. Silenced bool
  1057. CSRFField template.HTML
  1058. OauthSection bool
  1059. OauthAccounts []oauthAccountInfo
  1060. OauthSlack bool
  1061. OauthWriteAs bool
  1062. OauthGitLab bool
  1063. GitLabDisplayName string
  1064. OauthGeneric bool
  1065. OauthGenericDisplayName string
  1066. OauthGitea bool
  1067. GiteaDisplayName string
  1068. }{
  1069. UserPage: NewUserPage(app, r, u, "Account Settings", flashes),
  1070. Email: fullUser.EmailClear(app.keys),
  1071. HasPass: passIsSet,
  1072. IsLogOut: r.FormValue("logout") == "1",
  1073. Silenced: fullUser.IsSilenced(),
  1074. CSRFField: csrf.TemplateField(r),
  1075. OauthSection: displayOauthSection,
  1076. OauthAccounts: oauthAccounts,
  1077. OauthSlack: enableOauthSlack,
  1078. OauthWriteAs: enableOauthWriteAs,
  1079. OauthGitLab: enableOauthGitLab,
  1080. GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName),
  1081. OauthGeneric: enableOauthGeneric,
  1082. OauthGenericDisplayName: config.OrDefaultString(app.Config().GenericOauth.DisplayName, genericOauthDisplayName),
  1083. OauthGitea: enableOauthGitea,
  1084. GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName),
  1085. }
  1086. showUserPage(w, "settings", obj)
  1087. return nil
  1088. }
  1089. func loginViaEmail(app *App, alias, redirectTo string) error {
  1090. if !app.cfg.Email.Enabled() {
  1091. return fmt.Errorf("EMAIL ISN'T CONFIGURED on this server")
  1092. }
  1093. // Make sure user has added an email
  1094. // TODO: create a new func to just get user's email; "ForAuth" doesn't match here
  1095. u, _ := app.db.GetUserForAuth(alias)
  1096. if u == nil {
  1097. if strings.IndexAny(alias, "@") > 0 {
  1098. return ErrUserNotFoundEmail
  1099. }
  1100. return ErrUserNotFound
  1101. }
  1102. if u.Email.String == "" {
  1103. return impart.HTTPError{http.StatusPreconditionFailed, "User doesn't have an email address. Log in with password, instead."}
  1104. }
  1105. // Generate one-time login token
  1106. t, err := app.db.GetTemporaryOneTimeAccessToken(u.ID, 60*15, true)
  1107. if err != nil {
  1108. log.Error("Unable to generate token for email login: %s", err)
  1109. return impart.HTTPError{http.StatusInternalServerError, "Unable to generate token."}
  1110. }
  1111. // Send email
  1112. gun := mailgun.NewMailgun(app.cfg.Email.Domain, app.cfg.Email.MailgunPrivate)
  1113. toEmail := u.EmailClear(app.keys)
  1114. footerPara := "This link will only work once and expires in 15 minutes. Didn't ask us to log in? You can safely ignore this email."
  1115. plainMsg := fmt.Sprintf("Log in to %s here: %s/login?to=%s&with=%s\n\n%s", app.cfg.App.SiteName, app.cfg.App.Host, redirectTo, t, footerPara)
  1116. m := mailgun.NewMessage(app.cfg.App.SiteName+" <noreply-login@"+app.cfg.Email.Domain+">", "Log in to "+app.cfg.App.SiteName, plainMsg, fmt.Sprintf("<%s>", toEmail))
  1117. m.AddTag("Email Login")
  1118. m.SetHtml(fmt.Sprintf(`<html>
  1119. <body style="font-family:Lora, 'Palatino Linotype', Palatino, Baskerville, 'Book Antiqua', 'New York', 'DejaVu serif', serif; font-size: 100%%; margin:1em 2em;">
  1120. <div style="margin:0 auto; max-width: 40em; font-size: 1.2em;">
  1121. <h1 style="font-size:1.75em"><a style="text-decoration:none;color:#000;" href="%s">%s</a></h1>
  1122. <p style="font-size:1.2em;margin-bottom:1.5em;text-align:center"><a href="%s/login?to=%s&with=%s">Log in to %s here</a>.</p>
  1123. <p style="font-size: 0.86em;color:#666;text-align:center;max-width:35em;margin:1em auto">%s</p>
  1124. </div>
  1125. </body>
  1126. </html>`, app.cfg.App.Host, app.cfg.App.SiteName, app.cfg.App.Host, redirectTo, t, app.cfg.App.SiteName, footerPara))
  1127. _, _, err = gun.Send(m)
  1128. return err
  1129. }
  1130. func saveTempInfo(app *App, key, val string, r *http.Request, w http.ResponseWriter) error {
  1131. session, err := app.sessionStore.Get(r, "t")
  1132. if err != nil {
  1133. return ErrInternalCookieSession
  1134. }
  1135. session.Values[key] = val
  1136. err = session.Save(r, w)
  1137. if err != nil {
  1138. log.Error("Couldn't saveTempInfo for key-val (%s:%s): %v", key, val, err)
  1139. }
  1140. return err
  1141. }
  1142. func getTempInfo(app *App, key string, r *http.Request, w http.ResponseWriter) string {
  1143. session, err := app.sessionStore.Get(r, "t")
  1144. if err != nil {
  1145. return ""
  1146. }
  1147. // Get the information
  1148. var s = ""
  1149. var ok bool
  1150. if s, ok = session.Values[key].(string); !ok {
  1151. return ""
  1152. }
  1153. // Delete cookie
  1154. session.Options.MaxAge = -1
  1155. err = session.Save(r, w)
  1156. if err != nil {
  1157. log.Error("Couldn't erase temp data for key %s: %v", key, err)
  1158. }
  1159. // Return value
  1160. return s
  1161. }
  1162. func handleUserDelete(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  1163. if !app.cfg.App.OpenDeletion {
  1164. return impart.HTTPError{http.StatusForbidden, "Open account deletion is disabled on this instance."}
  1165. }
  1166. confirmUsername := r.PostFormValue("confirm-username")
  1167. if u.Username != confirmUsername {
  1168. return impart.HTTPError{http.StatusBadRequest, "Confirmation username must match your username exactly."}
  1169. }
  1170. // Check for account deletion safeguards in place
  1171. if u.IsAdmin() {
  1172. return impart.HTTPError{http.StatusForbidden, "Cannot delete admin."}
  1173. }
  1174. err := app.db.DeleteAccount(u.ID)
  1175. if err != nil {
  1176. log.Error("user delete account: %v", err)
  1177. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not delete account: %v", err)}
  1178. }
  1179. // FIXME: This doesn't ever appear to the user, as (I believe) the value is erased when the session cookie is reset
  1180. _ = addSessionFlash(app, w, r, "Thanks for writing with us! You account was deleted successfully.", nil)
  1181. return impart.HTTPError{http.StatusFound, "/me/logout"}
  1182. }
  1183. func removeOauth(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  1184. provider := r.FormValue("provider")
  1185. clientID := r.FormValue("client_id")
  1186. remoteUserID := r.FormValue("remote_user_id")
  1187. err := app.db.RemoveOauth(r.Context(), u.ID, provider, clientID, remoteUserID)
  1188. if err != nil {
  1189. return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()}
  1190. }
  1191. return impart.HTTPError{Status: http.StatusFound, Message: "/me/settings"}
  1192. }
  1193. func prepareUserEmail(input string, emailKey []byte) zero.String {
  1194. email := zero.NewString("", input != "")
  1195. if len(input) > 0 {
  1196. encEmail, err := data.Encrypt(emailKey, input)
  1197. if err != nil {
  1198. log.Error("Unable to encrypt email: %s\n", err)
  1199. } else {
  1200. email.String = string(encEmail)
  1201. }
  1202. }
  1203. return email
  1204. }