$npx -y skills add caffeinelabs/skills --skill extension-email-verificationSupport for sending an email with a link the recipient can click to prove they own the email address.
| 1 | # Email — Verification |
| 2 | Email verification extension for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral). |
| 3 | |
| 4 | ## Overview |
| 5 | |
| 6 | This skill adds email address verification via a click-to-verify link. The `MixinEmailVerification` handles the verification callback; `verifiedEmails` tracks verified addresses. |
| 7 | |
| 8 | # Backend |
| 9 | |
| 10 | ## This component is for sending an email to users with a verification link which the user can click to prove they own the email address. |
| 11 | |
| 12 | ### To check if an email address has been verified |
| 13 | |
| 14 | Use the prefabricated module `mo:caffeineai-email-verification/verifiedEmails.mo` which cannot be modified. |
| 15 | |
| 16 | ```mo:caffeineai-email-verification/verifiedEmails.mo |
| 17 | module { |
| 18 | public type State = { |
| 19 | var verifiedEmails : Set.Set<Text>; |
| 20 | }; |
| 21 | |
| 22 | public func new() : State { |
| 23 | { |
| 24 | var verifiedEmails = Set.empty<Text>(); |
| 25 | }; |
| 26 | }; |
| 27 | |
| 28 | public func contains(state : State, email : Text) : Bool; |
| 29 | |
| 30 | public func iter(state : State) : Iter.Iter<Text>; |
| 31 | |
| 32 | public func size(state : State) : Nat; |
| 33 | }; |
| 34 | ``` |
| 35 | |
| 36 | To check whether an email is verified use the `contains` function. Do NOT try to track the email verification status independently by storing it against the user profile. |
| 37 | |
| 38 | ### To handle the verification link |
| 39 | |
| 40 | Use the prefabricated module `mo:caffeineai-email-verification/verificationMixin.mo` which cannot be modified. |
| 41 | |
| 42 | The MixinEmailVerification handles calls to the verification link to verify an email address. |
| 43 | |
| 44 | ```mo:caffeineai-email-verification/verificationMixin.mo |
| 45 | import MixinEmailVerification "mo:caffeineai-email-verification/verificationMixin"; |
| 46 | ``` |
| 47 | |
| 48 | ### For sending users a verification email |
| 49 | |
| 50 | - This extension depends on the [extension-email](../extension-email/SKILL.md) for sending emails. |
| 51 | - Use the sendVerificationEmail function. |
| 52 | - It returns a SendResult which is #ok if the email is sent successfully otherwise #err(error) with the error text. |
| 53 | - Each recipient receives an individual email with a specific verification link for them |
| 54 | - The htmlBody MUST contain the placeholder text {{VERIFICATION_URL}} |
| 55 | |
| 56 | ```mo:caffeineai-email/emailClient.mo |
| 57 | module { |
| 58 | public type SendResult = { |
| 59 | #ok; |
| 60 | #err : Text; |
| 61 | }; |
| 62 | |
| 63 | public func sendVerificationEmail( |
| 64 | fromUsername : Text, |
| 65 | recipients : [Text], |
| 66 | subject : Text, |
| 67 | htmlBody : Text, |
| 68 | ) : async SendResult; |
| 69 | }; |
| 70 | ``` |
| 71 | |
| 72 | ### Example usage with endpoints for registering a user and for checking whether a user is verified. |
| 73 | |
| 74 | ```motoko filepath=src/backend/main.mo |
| 75 | import Map "mo:core/Map"; |
| 76 | import Runtime "mo:core/Runtime"; |
| 77 | import Principal "mo:core/Principal"; |
| 78 | import Text "mo:core/Text"; |
| 79 | import EmailClient "mo:caffeineai-email/emailClient"; |
| 80 | import MixinEmailVerification "mo:caffeineai-email-verification/verificationMixin"; |
| 81 | import VerifiedEmails "mo:caffeineai-email-verification/verifiedEmails"; |
| 82 | |
| 83 | actor { |
| 84 | // Stores which emails are verified |
| 85 | let verifiedEmails = VerifiedEmails.new(); |
| 86 | |
| 87 | // User profiles storage |
| 88 | let users = Map.empty<Principal, User>(); |
| 89 | |
| 90 | // Email to principal mapping for uniqueness check |
| 91 | let emailToPrincipal = Map.empty<Text, Principal>(); |
| 92 | |
| 93 | // Handles the verification link and updates the verifiedEmails store |
| 94 | include MixinEmailVerification(verifiedEmails); |
| 95 | |
| 96 | type User = { |
| 97 | name : Text; |
| 98 | email : Text; |
| 99 | }; |
| 100 | |
| 101 | public shared ({ caller }) func registerUser(email : Text, name : Text) : async () { |
| 102 | if (users.containsKey(caller)) { |
| 103 | Runtime.trap("User already registered"); |
| 104 | }; |
| 105 | if (emailToPrincipal.containsKey(email)) { |
| 106 | Runtime.trap("Email already registered"); |
| 107 | }; |
| 108 | |
| 109 | let user : User = { |
| 110 | name; |
| 111 | email; |
| 112 | }; |
| 113 | users.add(caller, user); |
| 114 | emailToPrincipal.add(email, caller); |
| 115 | let result = await EmailClient.sendVerificationEmail( |
| 116 | "no-reply", |
| 117 | [email], |
| 118 | "Welcome to Our Service", |
| 119 | "Hello " # name # ",<br><br>Thank you for registering with our service. Please <a href=\"{{VERIFICATION_URL}}\">click here</a> to verify your email address<br><br>Best regards,<br>The Team", |
| 120 | ); |
| 121 | |
| 122 | switch (result) { |
| 123 | case (#ok) {}; |
| 124 | case (#err(error)) { |
| 125 | Runtime.trap("Couldn't send verification email: " # error); |
| 126 | }; |
| 127 | }; |
| 128 | }; |
| 129 | |
| 130 | public shared ({ caller }) func isEmailVerified() : async Bool { |
| 131 | switch (users.get(caller)) { |
| 132 | case (null) { |
| 133 | Runtime.trap("User not registered"); |
| 134 | }; |
| 135 | case (?user) { |
| 136 | VerifiedEmails.contains(verifiedEmails, user.email); |
| 137 | }; |
| 138 | }; |
| 139 | }; |
| 140 | }; |
| 141 | ``` |
| 142 | |
| 143 | # Frontend |
| 144 | |
| 145 | If there is a UI for the admin to enter the content of a verification email then indicate that the placeholder text {{VERIFICATION_URL}} must be present in the email body. |