├── Gemfile ├── LICENSE ├── README.md ├── mailchimp.rb ├── messages └── 2021-july.md └── template.html /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gem "httparty" 3 | gem "kramdown" 4 | gem "front_matter_parser" 5 | gem "MailchimpMarketing" 6 | gem "roadie", "~> 4.0" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Robin Sloan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A minimalist Mailchimp CLI 2 | 3 | I have not created a campaign by hand in Mailchimp in six months, instead happily managing them using this script, so I thought I'd share it. 4 | 5 | Here's how it works. Start by drafting your email in `messages/`. The message's "slug" thereafter will be the filename before `.md`; the slug, for example, of the message included in this project is `2021-july`. 6 | 7 | To create the campaign in Mailchimp: 8 | 9 | `ruby mailchimp.rb 2021-july create` 10 | 11 | Then, send a test email to an address of your choosing: 12 | 13 | `ruby mailchimp.rb 2021-july test frodo@well.com` 14 | 15 | Spot an error? Edit the Markdown file, then update the campaign in Mailchimp: 16 | 17 | `ruby mailchimp.rb 2021-july update` 18 | 19 | You can also delete the campaign from Mailchimp (leaving the Markdown file intact): 20 | 21 | `ruby mailchimp.rb 2021-july delete` 22 | 23 | That's it! There is currently no way to send from the command line, because the thought of making a mistake freaked me out too much. Better to log in, give everything a final look, and send from the Mailchimp website. 24 | 25 | The template is in `template.html` and is presently very minimal. You can replace it, of course, with an HTML email template from elsewhere on Github or one of your own devising. Note the `__YIELD__`. 26 | 27 | The basic configuration, including your Mailchimp API key, is stored in a file called `config.yaml` which you'll have to create yourself. It has this form: 28 | 29 | ``` 30 | api_key: "foo" 31 | list_id: "bar" 32 | data_center: "blee" 33 | from_name: "Frodo Baggins" 34 | reply_to: "frodo@well.com" 35 | ``` 36 | 37 | Note that by `list_id` I mean the ID of your Mailchimp audience; [instructions for finding that ID are here](https://mailchimp.com/help/find-audience-id/). 38 | 39 | As you'll see, this system is designed to send campaigns to audience segments based on tags. That is *probably* not what you want, so you'll have to rip out that logic, which is easily done: just delete `"segment_opts" => segment_options` in `update_campaign_content`. 40 | 41 | I don't really expect this to be used as-is by anyone else, but it would have been a helpful starting point for me, six months ago, so now it can be a starting point for you! -------------------------------------------------------------------------------- /mailchimp.rb: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "kramdown" 3 | require "front_matter_parser" 4 | require "yaml/store" 5 | require "MailchimpMarketing" 6 | require "roadie" 7 | 8 | unless File.exist?("config.yaml") 9 | puts "You need to create a file called config.yaml that contains your Mailchimp API key and some other configuration info. Look in mailchimp.rb for a template." 10 | exit 11 | end 12 | 13 | # config.yaml template: 14 | # --- 15 | # api_key: "foo" 16 | # list_id: "bar" 17 | # data_center: "blee" 18 | # from_name: "Frodo Baggins" 19 | # reply_to: "frodo@well.com" 20 | 21 | CONFIG = YAML.load_file("config.yaml") 22 | API_KEY = CONFIG["api_key"] 23 | LIST_ID = CONFIG["list_id"] 24 | DATA_CENTER = CONFIG["data_center"] 25 | FROM_NAME = CONFIG["from_name"] 26 | REPLY_TO = CONFIG["reply_to"] 27 | 28 | $db = YAML::Store.new "campaigns.yaml" 29 | $client = MailchimpMarketing::Client.new 30 | $client.set_config( { api_key: API_KEY, server: DATA_CENTER } ) 31 | 32 | $tag_ids = {} 33 | tag_lookup = $client.lists.list_segments( LIST_ID ) 34 | tag_lookup["segments"].each do |segment| 35 | $tag_ids[segment["name"]] = segment["id"] 36 | end 37 | 38 | def update_tags 39 | $tag_ids.each do |tag_name, tag_id| 40 | $client.lists.update_segment( LIST_ID, tag_id, { "name" => tag_name } ) 41 | end 42 | end 43 | 44 | def create_new_campaign( with_slug: ) 45 | 46 | $db.transaction do 47 | if $db[with_slug] != nil 48 | puts "You already have a message with that slug!" 49 | return 50 | end 51 | end 52 | 53 | filename = "messages/#{with_slug}.md" 54 | 55 | unless File.exist?(filename) 56 | puts "There's no file at #{filename}..." 57 | return 58 | end 59 | 60 | parsed_content = FrontMatterParser::Parser.parse_file( filename ) 61 | 62 | header = parsed_content.front_matter 63 | 64 | response = $client.campaigns.create( 65 | { "type" => "regular", 66 | "recipients" => { "list_id" => LIST_ID }, 67 | "settings" => { "subject_line" => header["subject_line"], 68 | "title" => header["title"], 69 | "preview_text" => header["preview"], 70 | "from_name" => FROM_NAME, 71 | "reply_to" => REPLY_TO, 72 | "auto_footer" => false }, 73 | "tracking" => { "opens" => true, 74 | "html_clicks" => true } 75 | } ) 76 | 77 | campaign_id = response["id"] 78 | 79 | $db.transaction do 80 | unless $db[with_slug] 81 | $db[with_slug] = { "id" => campaign_id, 82 | "subject_line" => header["subject_line"], 83 | "title" => header["title"], 84 | "preview_text" => header["preview"], 85 | "created_at" => Time.now } 86 | end 87 | end 88 | 89 | update_campaign_content( with_slug: with_slug ) 90 | 91 | return campaign_id 92 | end 93 | 94 | TEMPLATE_PATH = "template.html" 95 | YIELD_STRING = "__YIELD__" 96 | 97 | # I'm putting this here, rather than in the template, because Markdown has 98 | # problems with the pipe character used in Mailchimp's special codes and 99 | # this was the easiest way to solve the problem! 100 | 101 | UNSUBSCRIBE_CONTENT_HTML = <<~HEREDOC 102 |