···55 | _ ->
66 false
7788-let parse_email_change_code (actor : Data_store.Types.actor) =
88+let parse_email_change_code code =
99+ if String.starts_with ~prefix:"eml-" code then
1010+ let rest =
1111+ String.sub code 4 (String.length code - 4)
1212+ |> Base64.decode_exn ~alphabet:Base64.uri_safe_alphabet ~pad:false
1313+ in
1414+ match String.split_on_char ':' rest with
1515+ | [token; new_email] ->
1616+ Some (token, new_email)
1717+ | _ ->
1818+ None
1919+ else None
2020+2121+let validate_actor_email_code (actor : Data_store.Types.actor) =
922 match (actor.auth_code, actor.auth_code_expires_at) with
1023 | Some code, Some expires_at when expires_at > Util.now_ms () ->
1111- if String.starts_with ~prefix:"eml-" code then
1212- let rest = String.sub code 6 (String.length code - 6) in
1313- match String.index_opt rest ':' with
1414- | Some idx ->
1515- let token = String.sub rest 0 idx in
1616- let new_email =
1717- String.sub rest (idx + 1) (String.length rest - idx - 1)
1818- in
1919- Some (token, new_email)
2020- | None ->
2121- None
2222- else None
2424+ parse_email_change_code code
2325 | _ ->
2426 None
2527···4143 in
4244 let csrf_token = Dream.csrf_token ctx.req in
4345 let deactivated = actor.deactivated_at <> None in
4444- let email_change_info = parse_email_change_code actor in
4646+ let email_change_info = validate_actor_email_code actor in
4547 let email_change_pending = Option.is_some email_change_info in
4648 let pending_email = Option.map snd email_change_info in
4749 let delete_pending = has_valid_delete_code actor in
···8587 in
8688 let actor = Option.get actor_opt in
8789 let deactivated = actor.deactivated_at <> None in
8888- let email_change_info = parse_email_change_code actor in
9090+ let email_change_info = validate_actor_email_code actor in
8991 let email_change_pending = Option.is_some email_change_info in
9092 let pending_email = Option.map snd email_change_info in
9193 let delete_pending = has_valid_delete_code actor in
···173175 let%lwt () =
174176 Data_store.set_auth_code ~did ~code ~expires_at ctx.db
175177 in
176176- (* TODO: send email with code *)
177177- Dream.log "delete account code for %s: %s" did code ;
178178+ let%lwt () =
179179+ Util.send_email_or_log ~recipients:[To actor.email]
180180+ ~subject:"Account deletion confirmation"
181181+ ~body:
182182+ (Plain
183183+ (Printf.sprintf
184184+ "Confirm that you would like to delete the \
185185+ account %s (%s) by entering the following \
186186+ code: %s"
187187+ actor.handle did code ) )
188188+ in
178189 render_page ()
179190 | Some "confirm_delete" -> (
180191 let token =
···219230 ()
220231 | None ->
221232 let token = Mist.Tid.now () in
222222- let code = "eml-" ^ token ^ ":" ^ new_email in
233233+ let code = token ^ ":" ^ new_email in
234234+ let code =
235235+ Base64.encode_exn
236236+ ~alphabet:Base64.uri_safe_alphabet ~pad:false
237237+ code
238238+ in
239239+ let code = "eml-" ^ code in
223240 let expires_at =
224241 Util.now_ms () + (15 * 60 * 1000)
225242 in
···227244 Data_store.set_auth_code ~did ~code ~expires_at
228245 ctx.db
229246 in
230230- (* TODO: send email with code *)
231231- Dream.log "email change code for %s: %s" actor.email
232232- code ;
247247+ let%lwt () =
248248+ Util.send_email_or_log
249249+ ~recipients:[To actor.email]
250250+ ~subject:"Email change confirmation"
251251+ ~body:
252252+ (Plain
253253+ (Printf.sprintf
254254+ "Confirm that you would like to update \
255255+ the email address for @%s (%s) from \
256256+ %s to %s by entering the following \
257257+ code: %s"
258258+ actor.handle did actor.email new_email
259259+ code ) )
260260+ in
233261 render_page () )
234262 | Some "confirm_email_change" -> (
235263 let token =
236264 List.assoc_opt "token" fields
237265 |> Option.value ~default:"" |> String.trim
238266 in
239239- match parse_email_change_code actor with
240240- | Some (stored_token, new_email) when stored_token = token
241241- ->
267267+ match validate_actor_email_code actor with
268268+ | Some (_, new_email) when Some token = actor.auth_code ->
242269 let%lwt () =
243270 Data_store.update_email ~did ~email:new_email ctx.db
244271 in
···1010 in
1111 let expires_at = Util.now_ms () + (60 * 60 * 1000) in
1212 let%lwt () = Data_store.set_auth_code ~did ~code ~expires_at db in
1313- (* TODO: something that isn't this *)
1414- Dream.log "auth code for %s: %s" did code ;
1313+ let%lwt {email; handle; _} =
1414+ Data_store.get_actor_by_identifier did db |> Lwt.map Option.get
1515+ in
1616+ let%lwt () =
1717+ Util.send_email_or_log ~recipients:[To email]
1818+ ~subject:"Confirm PLC operation"
1919+ ~body:
2020+ (Plain
2121+ (Printf.sprintf
2222+ "Confirm that you would like to update your PLC identity for \
2323+ %s (%s) using the following token: %s"
2424+ handle did code ) )
2525+ in
1526 Dream.empty `OK )
+11-2
pegasus/lib/api/server/requestAccountDelete.ml
···44 match%lwt Data_store.get_actor_by_identifier did db with
55 | None ->
66 Errors.internal_error ~msg:"actor not found" ()
77- | Some _actor ->
77+ | Some actor ->
88 let code =
99 "del-"
1010 ^ String.sub
···1515 in
1616 let expires_at = Util.now_ms () + (15 * 60 * 1000) in
1717 let%lwt () = Data_store.set_auth_code ~did ~code ~expires_at db in
1818- Dream.log "account deletion code for %s: %s" did code ;
1818+ let%lwt () =
1919+ Util.send_email_or_log ~recipients:[To actor.email]
2020+ ~subject:
2121+ (Printf.sprintf "Account deletion request for %s" actor.handle)
2222+ ~body:
2323+ (Plain
2424+ (Printf.sprintf
2525+ "Delete your account using the following token: %s" code )
2626+ )
2727+ in
1928 Dream.empty `OK )
···3737 ( Base64.(encode ~alphabet:uri_safe_alphabet ~pad:false) secret
3838 |> Result.get_ok ) ) ;
3939 Bytes.of_string secret
4040+4141+let smtp_config, smtp_sender =
4242+ begin
4343+ let with_starttls =
4444+ Option.value ~default:"false" @@ Sys.getenv_opt "SMTP_STARTTLS" = "true"
4545+ in
4646+ match
4747+ ( Option.map Uri.of_string (Sys.getenv_opt "SMTP_AUTH_URI")
4848+ , Sys.getenv_opt "SMTP_SENDER" )
4949+ with
5050+ | Some uri, Some sender -> (
5151+ match
5252+ ( Uri.scheme uri
5353+ , Uri.user uri
5454+ , Uri.password uri
5555+ , Uri.host uri
5656+ , Uri.port uri )
5757+ with
5858+ | Some scheme, Some username, Some password, Some hostname, port
5959+ when scheme = "smtp" || scheme = "smtps" -> (
6060+ match Emile.of_string sender with
6161+ | Ok _ ->
6262+ ( Some
6363+ Letters.Config.(
6464+ create ~username ~password ~hostname ~with_starttls ()
6565+ |> set_port port )
6666+ , Some sender )
6767+ | Error _ ->
6868+ failwith
6969+ "SMTP_SENDER should be a valid mailbox, e.g. `e@mail.com` or \
7070+ `Name <e@mail.com>`" )
7171+ | _ ->
7272+ failwith
7373+ "SMTP_AUTH_URI must be a valid smtp:// or smtps:// URI with \
7474+ username, password, and hostname" )
7575+ | Some _, None ->
7676+ failwith
7777+ "SMTP_SENDER must be set alongside SMTP_AUTH_URI; it should look \
7878+ like `e@mail.com` or `Name <e@mail.com>`"
7979+ | None, Some _ ->
8080+ failwith "SMTP_AUTH_URI must be set alongside SMTP_SENDER"
8181+ | None, None ->
8282+ (None, None)
8383+ end
+25
pegasus/lib/util.ml
···486486let make_at_uri ~repo ~collection ~rkey ~fragment =
487487 Printf.sprintf "at://%s/%s/%s%s" repo collection rkey
488488 (Option.value ~default:"" fragment)
489489+490490+let send_email_or_log ~(recipients : Letters.recipient list) ~subject
491491+ ~(body : Letters.body) =
492492+ let log_email () =
493493+ match body with
494494+ | Plain text | Html text | Mixed (text, _, _) ->
495495+ let to_addr =
496496+ List.find_map
497497+ (fun (r : Letters.recipient) ->
498498+ match r with To addr -> Some addr | _ -> None )
499499+ recipients
500500+ |> Option.get
501501+ in
502502+ Dream.log "email to %s: %s" to_addr text
503503+ in
504504+ match (Env.smtp_config, Env.smtp_sender) with
505505+ | Some config, Some sender -> (
506506+ match Letters.create_email ~from:sender ~recipients ~subject ~body () with
507507+ | Error e ->
508508+ failwith (Printf.sprintf "failed to construct email: %s" e)
509509+ | Ok message -> (
510510+ try%lwt Letters.send ~config ~sender ~recipients ~message
511511+ with _ -> Lwt.return (log_email ()) ) )
512512+ | _ ->
513513+ Lwt.return (log_email ())