Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Send personalised marketing emails to subscribers with an unsubscribe link.
.claude/skills/aiskillstore-extension-email-marketing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 147% | 0% |
| case-01 | ✗→✓ | ▲ Improved | -9% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 18% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 14% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 30% | 0% |
Marketing email extension for Caffeine AI.
This skill adds direct marketing email support with subscriber management, topic-based subscriptions, and automatic unsubscribe links. Requires email verification before users can receive marketing emails.
mo:caffeineai-email-marketing/subscribers.mo which cannot be modified.mo:caffeineai-email-marketing/subscribers.momodule { public type State = { var topics : Map.Map<Nat, TopicRecord>; var topicsByName : Map.Map<Text, Nat>; }; // Add a new topic by name or get an existing topic if it already exists. Returns the topic ID. public func addTopic(state : State, name : Text) : Nat; // Rename a topic. Returns false if a topic with the new name already exists or the topic ID does not exist. public func renameTopic(state : State, topicId : Nat, newName : Text) : Bool; // Remove a topic. Also removes all subscribers from that topic. public func removeTopic(state : State, topicId : Nat); // List all topics (id, name). public func listTopics(state : State) : [Topic]; // Get a topic ID by name public func getTopicId(state : State, name : Text) : ?Nat; // Get a topic name by ID public func getTopicName(state : State, topicId : Nat) : ?Text; // Add a subscriber to a topic. Returns false if the topic doesn't exist public func add(state : State, topicId : Nat, email : Text) : Bool; // Remove a subscriber from a topic public func remove(state : State, topicId : Nat, email : Text); // Remove a subscriber from all topics public func removeFromAllTopics(state : State, email : Text); // List all subscribers alongside their verification status for a given topic. Returns null if the topic doesn't exist. public func list(state : State, verifiedEmails : VerifiedEmails.State, topicId : Nat) : ?[(Text, Bool)]; // List all verified subscribers for a given topic. Returns null if the topic doesn't exist. public func verified(state : State, verifiedEmails : VerifiedEmails.State, topicId : Nat) : ?[Text]; // Return whether a subscriber is subscribed to a topic public func isSubscribed(state : State, topicId : Nat, email : Text) : Bool; // List all topics a subscriber is subscribed to. public func listTopicsForSubscriber(state : State, email : Text) : [Topic]; // Returns the count of subscribers for a given topic public func count(state : State, topicId : Nat) : Nat; // Returns the count of verified subscribers for a given topic public func verifiedCount(state : State, verifiedEmails : VerifiedEmails.State, topicId : Nat) : Nat; };
Use the prefabricated module caffeineai-email-marketing/unsubscribeMixin.mo which cannot be modified.
The MixinEmailUnsubscribe module handles calls to the unsubscribe link to unsubscribe an email address from a topic.
mo:caffeineai-email-marketing/unsubscribeMixin.moimport MixinEmailUnsubscribe "mo:caffeineai-email-marketing/unsubscribeMixin";
email component for sending email addresses.recipients argument is an array of email addresses, where for each email an optional array of substitution name/value pairs can be specified.mo:caffeineai-email/emailClient.momodule { public type BroadcastEmailRecipient = { email : Text; substitutions : ?[(Text, Text)]; }; public type SendResult = { #ok; #err : Text; }; public func sendMarketingEmail( topicId : Nat, fromUsername : Text, recipients : [BroadcastEmailRecipient], subject : Text, htmlBody : Text, ) : async SendResult; };
motoko filepath=src/backend/main.moimport Array "mo:core/Array"; import Runtime "mo:core/Runtime"; import Option "mo:core/Option"; import Principal "mo:core/Principal"; import Iter "mo:core/Iter"; import Map "mo:core/Map"; import Set "mo:core/Set"; import Text "mo:core/Text"; import AccessControl "mo:caffeineai-authorization/access-control"; import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization"; import EmailClient "mo:caffeineai-email/emailClient"; import MixinEmailUnsubscribe "mo:caffeineai-email-marketing/unsubscribeMixin"; import EmailSubscribers "mo:caffeineai-email-marketing/subscribers"; import MixinEmailVerification "mo:caffeineai-email-verification/verificationMixin"; import VerifiedEmails "mo:caffeineai-email-verification/verifiedEmails"; actor { public type UserProfile = { name : Text; email : Text; }; // Include authorization component let accessControlState = AccessControl.initState(); include MixinAuthorization(accessControlState); // Store a map of caller principal to UserProfile let userProfiles = Map.empty<Principal, UserProfile>(); // Store a set of emails for uniqueness check let emails = Set.empty<Text>(); // Stores which emails are verified let verifiedEmails = VerifiedEmails.new(); // In this example we use a single hardcoded topic. // In general there could be CRUD endpoints for the admin to manage email subscription topics. let newsletterTopic = "Newsletter"; // Store the email subscribers per topic let emailSubscribers = EmailSubscribers.new([newsletterTopic]); // Include this mixin to handle the unsubscribe link which updates the EmailSubscribers state include MixinEmailUnsubscribe(emailSubscribers); // Include this mixin to handle the verification link which updates the VerifiedEmails state include MixinEmailVerification(verifiedEmails); func getUserInternal(caller : Principal) : UserProfile { switch (userProfiles.get(caller)) { case (null) { Runtime.trap("User profile does not exist!") }; case (?userProfile) { userProfile }; }; }; public shared ({ caller }) func registerUser(name : Text, email : Text) : async () { // Check if the user already exists if (userProfiles.containsKey(caller)) { Runtime.trap("User already registered"); }; // Check if the email is already used if (emails.contains(email)) { Runtime.trap("Email already taken"); }; // Add a user record userProfiles.add( caller, { name; email; }, ); emails.add(email); // Subscribe the user to the Newsletter topic by default switch (EmailSubscribers.getTopicId(emailSubscribers, newsletterTopic)) { case (null) { Runtime.trap("Newsletter topic not found") }; case (?topicId) { ignore EmailSubscribers.add(emailSubscribers, topicId, email); }; }; // Send a verification email let result = await EmailClient.sendVerificationEmail( "no-reply", [email], "Welcome to Our Service", "Hello " # name # ",<br><br>Thank you for registering with our service.<br><br>Please <a href=\"{{VERIFICATION_URL}}\">click here</a> to verify your email address.<br><br>By clicking on the verification link you also agree to sign-up to the monthly Newsletter which you can unsubscribe from at any time.<br><br>Best regards,<br>The Team", ); switch (result) { case (#ok) {}; case (#err(error)) { Runtime.trap("Failed to send verification email: " # error); }; }; }; public shared ({ caller }) func addTopic(name : Text) : async Nat { if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) { Runtime.trap("Unauthorized: Only admins can add topics"); }; EmailSubscribers.addTopic(emailSubscribers, name); }; public shared ({ caller }) func removeTopic(topicId : Nat) : async () { if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) { Runtime.trap("Unauthorized: Only admins can remove topics"); }; EmailSubscribers.removeTopic(emailSubscribers, topicId); }; public shared ({ caller }) func renameTopic(topicId : Nat, newName : Text) : async () { if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) { Runtime.trap("Unauthorized: Only admins can rename topics"); }; let success = EmailSubscribers.renameTopic(emailSubscribers, topicId, newName); if (not success) { Runtime.trap("Failed to rename topic"); }; }; public shared ({ caller }) func subscribeToTopic(topicId : Nat) : async () { let userProfile = getUserInternal(caller); ignore EmailSubscribers.add(emailSubscribers, topicId, userProfile.email); }; public shared ({ caller }) func unsubscribeFromTopic(topicId : Nat) : async () { let userProfile = getUserInternal(caller); EmailSubscribers.remove(emailSubscribers, topicId, userProfile.email); }; public shared ({ caller }) func sendMarketingEmail(topicId : Nat, subject : Text, htmlBody : Text) : async () { if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) { Runtime.trap("Unauthorized: Only admins can send the newsletter"); }; // Get the array of subscriber emails that have been verified let recipientEmails = switch (EmailSubscribers.verified(emailSubscribers, verifiedEmails, topicId)) { case (null) { Runtime.trap("No verified subscribers found for newsletter topic"); }; case (?recipientEmails) { recipientEmails }; }; if (recipientEmails.size() == 0) { Runtime.trap("No verified subscribers found for newsletter topic"); }; // Ensure the email body contains the unsubscribe link placeholder let finalHtmlBody = if (htmlBody.contains(#text "{{UNSUBSCRIBE_URL}}")) { htmlBody; } else { htmlBody # "<br><br>To unsubscribe <a href=\"{{UNSUBSCRIBE_URL}}\">click here</a>"; }; // For each recipient specify the NAME substitution to personalise the email let recipients = recipientEmails.filterMap( func(email) { switch (userProfiles.values().find(func(user) { user.email == email })) { case (?user) { ?{ email; substitutions = ?[("NAME", user.name)] } }; case (null) { null }; }; } ); let result = await EmailClient.sendMarketingEmail( topicId, "no-reply", recipients, subject, finalHtmlBody, ); switch (result) { case (#ok) {}; case (#err(error)) { Runtime.trap("Failed to send newsletter: " # error); }; }; }; public query ({ caller }) func listTopics() : async [EmailSubscribers.Topic] { EmailSubscribers.listTopics(emailSubscribers); }; // Admin function to list topic subscribers and whether the email is verified or not public query ({ caller }) func listSubscribers(topicId : Nat) : async [(Text, Bool)] { if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) { Runtime.trap("Unauthorized: Only admins can list topic subscribers"); }; EmailSubscribers.list(emailSubscribers, verifiedEmails, topicId).get([]); }; public query ({ caller }) func isCallerSubscribedToTopic(topicId : Nat) : async Bool { let userProfile = getUserInternal(caller); EmailSubscribers.listTopicsForSubscriber(emailSubscribers, userProfile.email).find( func(topic) { topic.id == topicId } ).isSome(); }; public query ({ caller }) func isCallerEmailVerified() : async Bool { let userProfile = getUserInternal(caller); VerifiedEmails.contains(verifiedEmails, userProfile.email); }; };
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-11 | fail→pass | 15,121 | 4,435 | -71% | 1 | 1 | 0% | 1,666 | 4,111 | +147% | 0 | 0 | — |
case-01 | fail→pass | 41,740 | 22,922 | -45% | 1 | 1 | 0% | 8,257 | 7,534 | -9% | 0 | 0 | — |
case-02 | fail→pass | 29,785 | 17,263 | -42% | 1 | 1 | 0% | 4,536 | 5,359 | +18% | 0 | 0 | — |
case-03 | fail→pass | 36,309 | 19,795 | -45% | 1 | 1 | 0% | 6,110 | 6,947 | +14% | 0 | 0 | — |
case-04 | pass→pass | 17,930 | 8,006 | -55% | 1 | 1 | 0% | 2,187 | 3,791 | +73% | 0 | 0 | — |
case-05 | fail→pass | 21,428 | 12,491 | -42% | 1 | 1 | 0% | 3,547 | 4,614 | +30% | 0 | 0 | — |
case-06 | fail→pass | 13,375 | 11,242 | -16% | 1 | 1 | 0% | 1,906 | 4,190 | +120% | 0 | 0 | — |
case-07 | fail→pass | 12,381 | 1,614 | -87% | 1 | 1 | 0% | 1,237 | 3,540 | +186% | 0 | 0 | — |
case-08 | fail→pass | 15,736 | 3,655 | -77% | 1 | 1 | 0% | 1,862 | 3,991 | +114% | 0 | 0 | — |
case-09 | fail→pass | 12,537 | 4,433 | -65% | 1 | 1 | 0% | 2,070 | 4,159 | +101% | 0 | 0 | — |
case-10 | fail→pass | 17,513 | 3,557 | -80% | 1 | 1 | 0% | 1,924 | 3,814 | +98% | 0 | 0 | — |
case-12 | fail→pass | 11,508 | 7,514 | -35% | 1 | 1 | 0% | 1,825 | 3,712 | +103% | 0 | 0 | — |
case-13 | fail→fail | 11,927 | 5,723 | -52% | 1 | 1 | 0% | 2,148 | 4,177 | +94% | 0 | 0 | — |
case-14 | fail→pass | 16,617 | 6,969 | -58% | 1 | 1 | 0% | 2,934 | 4,703 | +60% | 0 | 0 | — |
case-15 | fail→pass | 17,846 | 3,196 | -82% | 1 | 1 | 0% | 2,103 | 3,726 | +77% | 0 | 0 | — |
case-16 | fail→fail | 16,314 | 15,117 | -7% | 1 | 1 | 0% | 2,615 | 4,833 | +85% | 0 | 0 | — |
case-17 | fail→pass | 13,271 | 1,847 | -86% | 1 | 1 | 0% | 2,092 | 3,598 | +72% | 0 | 0 | — |
case-18 | fail→pass | 17,354 | 9,146 | -47% | 1 | 1 | 0% | 2,192 | 5,144 | +135% | 0 | 0 | — |
case-19 | fail→pass | 11,394 | 8,503 | -25% | 1 | 1 | 0% | 1,686 | 3,779 | +124% | 0 | 0 | — |
case-20 | pass→pass | 11,990 | 7,288 | -39% | 1 | 1 | 0% | 1,111 | 3,654 | +229% | 0 | 0 | — |
case-21 | pass→pass | 60,101 | 12,004 | -80% | 1 | 1 | 0% | 3,645 | 5,739 | +57% | 0 | 0 | — |
case-22 | pass→pass | 14,869 | 15,963 | +7% | 1 | 1 | 0% | 2,621 | 5,435 | +107% | 0 | 0 | — |
case-23 | pass→pass | 27,473 | 36,722 | +34% | 1 | 1 | 0% | 4,350 | 6,891 | +58% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +70 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.