├── .github └── dependabot.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── .travis.yml ├── CONTRIBUTING.markdown ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── Rakefile ├── TODO.markdown ├── bin ├── task-uuid ├── twmail └── twmail-hook ├── doc └── fetchmailrc.sample ├── lib ├── extensions │ └── string.rb ├── twmail.rb └── twmail │ └── version.rb ├── test ├── fixtures │ ├── mail_empty.txt │ ├── mail_multipart.txt │ ├── mail_regular.txt │ ├── mail_separator.txt │ ├── mail_with_signature.txt │ ├── task01.json │ └── task02.json ├── helpers │ ├── assertions │ └── test_hook ├── test_helper.rb └── unit │ ├── test_mail.rb │ ├── test_task_uuid.rb │ ├── test_twmail.rb │ └── test_twmail_hooks └── twmail.gemspec /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "02:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | test/tmp 15 | test/version_tmp 16 | tmp 17 | .cache_rake_t 18 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.7 3 | Include: 4 | - '**/Gemfile' 5 | - '**/Rakefile' 6 | - '**/*.rake' 7 | Exclude: 8 | - vendor/**/* 9 | - db/migrations/**/* 10 | 11 | DisplayCopNames: 12 | Enabled: true 13 | 14 | DisplayStyleGuide: 15 | Enabled: true 16 | 17 | Metrics/BlockLength: 18 | Exclude: 19 | - spec/**/* 20 | 21 | Layout/LineLength: 22 | Max: 180 23 | 24 | Layout/SpaceAroundMethodCallOperator: 25 | Enabled: true 26 | 27 | Lint/RaiseException: 28 | Enabled: true 29 | 30 | Lint/StructNewOverride: 31 | Enabled: true 32 | 33 | Style/ExponentialNotation: 34 | Enabled: true 35 | 36 | Style/HashEachMethods: 37 | Enabled: true 38 | 39 | Style/HashTransformKeys: 40 | Enabled: true 41 | 42 | Style/HashTransformValues: 43 | Enabled: true 44 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: ruby 3 | rvm: 4 | - 2.7.1 5 | - 2.6.6 6 | - 2.5.8 7 | 8 | before_script: 9 | - mkdir ~/.task 10 | - echo data.location=~/.task > ~/.taskrc 11 | 12 | before_install: 13 | - sudo apt-get install task 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.markdown: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork it 4 | 2. Create your feature branch (`git checkout -b my-new-feature`) 5 | 3. Commit your changes (`git commit -am 'Added some feature'`) 6 | 4. Push to the branch (`git push origin my-new-feature`) 7 | 5. Create new Pull Request 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # use local twtest while developing 6 | # group :development do 7 | # gem 'twtest', path: '../twtest' 8 | # end 9 | 10 | gemspec 11 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | guard 'bundler' do 4 | watch('Gemfile') 5 | watch(/^.+\.gemspec/) 6 | end 7 | 8 | guard :test, test_paths: ['test/unit'] do 9 | watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" } 10 | watch(%r{^test/unit/test_(.+)\.rb$}) 11 | watch('test/test_helper.rb') { 'test' } 12 | watch('test/helpers/**/*') { 'test' } 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Nicholas E. Rabenau 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # twmail 2 | 3 | [![Build Status](https://travis-ci.org/nerab/TaskWarriorMail.svg?branch=master)](https://travis-ci.org/nerab/TaskWarriorMail) 4 | 5 | `twmail` allows you to mail new tasks to your TaskWarrior inbox. 6 | 7 | ## Installation 8 | 9 | $ gem install twmail 10 | 11 | ## Usage 12 | 13 | 1. Install ruby and this gem 14 | 1. If you don't have a `~/.fetchmailrc` yet, copy `doc/fetchmailrc.sample` to `~/.fetchmailrc` 15 | 1. Edit `~/.fetchmailrc` and adjust mail account settings (the example was made for Google Mail account). If in doubt, consult the `fetchmail` documentation, e.g. by executing `man fetchmailconf` in a terminal. 16 | 17 | If Docker is your thing, check out [eyenx/docker-taskwarriormail](https://github.com/eyenx/docker-taskwarriormail). 18 | 19 | ## Motivation 20 | 21 | I would like to add new tasks to my TaskWarrior inbox from remote places where I don't have immediate access to my personal TaskWarrior database; e.g. from my iPhone, from work (where I don't have access to my personal TaskWarrior installation) or from another computer. 22 | 23 | Using eMail for this looks like a great candidate: 24 | 25 | 1. I don't want to (or cannot) install TaskWarrior on all the places and machines where I would like to add tasks from. Sending a note as eMail is pretty much universally available. 26 | 1. Many applications were not made for integration with TaskWarrior. But even the dumbest iPhone app can forward text or a URL via eMail. 27 | 1. eMail is asynchronous by design (fire and forget). Even if disconnected from the net, I can send eMail and the system will deliver it on the very next occassion. 28 | 29 | What is missing from a TaskWarrior perspective right now is a way to add these mails to a TaskWarrior installation automatically. 30 | 31 | ## Architecture 32 | 33 | The simplest solution I could come up with is this: 34 | 35 | 1. A dedicated email account is used to collect the tasks. 36 | 1. A script that imports all eMails as new tasks. 37 | 38 | As a prerequisite, TaskWarrior is assumed to be installed and configured. With this architecture in place, the functionality is rather simple to implement: 39 | 40 | For each mail{ 41 | Transaction{ 42 | * Fetch mail from mailbox 43 | * Store mail as new task in Taskwarrior 44 | * Delete mail from mailbox 45 | } 46 | } 47 | 48 | As the word `Transaction` implies, the whole operation needs to be atomic per mail. No task must be added if fetching a mail went wrong, and no mail must be deleted if storing the task in TaskWarrior failed. 49 | 50 | The solution presented here maintains a one-to-one relation between the INBOX of an mail account and the TaskWarrior database. 51 | 52 | ## Components 53 | 54 | Mail fetching is done with `fetchmail`, a proven solution available on all major Unices incl. MacOS. It will be configured to use the `twmail` script as a mail delivery agent (mda), which means nothing more than that `fetchmail` fetches the mail from the configured account and hands it over to our script. There is no further storage of the received mails except in TaskWarrior. 55 | 56 | ## Error Handling 57 | 58 | If our MDA returns non-zero, `fetchmail` will not assume the message to be processed and it will try again. 59 | 60 | ## Alternatives 61 | 62 | One might think of more elaborate applications that do more clever things, but I wanted to create this solution with as few external dependencies as possible. `fetchmail` is available on all Unices, and who can afford to live without TaskWarrior anyway? I also played with the thought of a central tasks server that receives mail from services like CloudMailIn and auto-adds them to the server, but the result would not be much different (besides being more complex) to the solution presented here: No task will be fetched into TaskWarrior until the machine with the TaskWarrior database is online. 63 | 64 | Another alternative would be to convert the email to JSON and use TaskWarrior's import command. This would allow to create and annotate a new task in one step without the `bin/task-uuid` workaround. 65 | 66 | ## Advanced Usage 67 | 68 | ### Filtering and Routing 69 | 70 | Many more advanced use cases like filtering and routing can be implemented on the mail server side. There are plenty of user interfaces for routing eMails based on their subject, sender, body text, etc. The simplest way to integrate these features with `twmail` is to use IMAP folders. After all filtering and routing, each eMail must end up in a dedicated IMAP folder (by default, all tasks are fetched from the INBOX folder). `twmail` can then be configured to do different things depending on which IMAP folder a mail came from. 71 | 72 | As an example, here is a simple way to route eMails to different projects in TaskWarrior, based on their subject line: 73 | 74 | 1. Set up a dedicated IMAP folder for every project you work on, e.g. "Build Bikeshed", "Reading List", "Get Rich Fast" 75 | 1. Configure the mail server to move every mail from INBOX to the 76 | 1. "Build Bikeshed" folder if the mail subject contains "project:Bikeshed" 77 | 1. "Reading List" folder if the mail subject contains "project:Reading" 78 | 1. "Get Rich Fast" folder if the mail subject contains "project:GetRichFast" 79 | 1. Tell `twmail` to fetch mails from the "Build Bikeshed", "Reading List", and "Get Rich Fast" IMAP folders (in addition to the INBOX): 80 | 81 | The approach chosen for `twmail` also addresses SPAM filtering. Handling that remains the responsibility of the mail server. Anything that makes it to the INBOX is treated as task. 82 | 83 | ### Hooks 84 | 85 | `twmail` comes with an advanced implementation that supports hooks. This makes handling incoming mail very simple for someone familiar with shell scripting, and there is no need to edit the `twmail` scripts in order to customize its behavior. 86 | 87 | When `fetchmail` is configured to use `twmail-hook` instead of `twmail`, the script will call the `twmail-hook` command (must be in the user's `$PATH`). Within the hook script, the fields of the parsed email are available as environment variables: 88 | 89 | TWMAIL_DATE 90 | TWMAIL_MESSAGE_ID 91 | TWMAIL_FROM 92 | TWMAIL_TO 93 | TWMAIL_SUBJECT 94 | TWMAIL_BODY 95 | 96 | Have a look at test/helpers/test_hook for a very simple implementation. 97 | 98 | If you prefer a hook with a different name, specify it in the `TWMAIL_HOOK` environment variable in your `.fetchmailrc`. For example, if your home directory contains a script called `taskwarrior-import.sh`, edit the `mda` line to look like this: 99 | 100 | mda TWMAIL_HOOK=~/taskwarrior-import.sh twmail-hook 101 | 102 | ## Housekeeping 103 | 104 | By default `fetchmail` will mark retrieved messages as read, but leave them on the server. For housekeeping purposes, it may be desirable to delete messages from the server once they were successfully imported into TaskWarrior. 105 | 106 | There are two ways to achieve this: 107 | 108 | 1. Create a filter on the server side that deletes all read mail to a dedicated folder (perhaps "Archive" or "Trash"), or simply deletes it. 109 | 1. Run `fetchmail` with the `--nokeep` option, which will delete retrieved messages from the server. 110 | 111 | Which option to choose depends on the capabilities of your mail server (Google Mail cannot handle mails based on their read status), and on your level of trust in `twmail`. I recommend leaving mails on the server until you are confident that everything works as expected. 112 | 113 | ## Testing 114 | 115 | `twmail` comes with a basic set of tests. Execute them by running `rake` in the cloned source repo. 116 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rake/testtask' 5 | require 'rubocop/rake_task' 6 | RuboCop::RakeTask.new 7 | 8 | Rake::TestTask.new(:test) do |test| 9 | test.libs << 'lib' << 'test' << 'test/helpers' 10 | test.test_files = FileList['test/**/test_*.rb'] 11 | end 12 | 13 | task default: %i[rubocop test] 14 | -------------------------------------------------------------------------------- /TODO.markdown: -------------------------------------------------------------------------------- 1 | * Describe how to use fetchmail's daemon mode 2 | * Describe how to use fetchmail's IMAP IDLE flag 3 | * Do we need a dedicated dead-letter queue for all mails fetched, but not successfully processed? 4 | * Consider the [gmail gem](https://github.com/gmailgem/gmail) instead of fetchmail 5 | -------------------------------------------------------------------------------- /bin/task-uuid: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'securerandom' 5 | require 'optparse' 6 | require 'twmail' 7 | 8 | OptionParser.new do |opts| 9 | banner = <<~HERE 10 | #{opts.program_name} creates a new TaskWarrior task and prints its UUID to STDOUT using the following approach: 11 | 12 | 1. Make up a temporary tag name that is very unlikely to exist yet. 13 | 14 | 2. Create the new task tagged with our temporary tag 15 | 16 | 3. Use the name of our temporary tag to find the actual UUID generated by TaskWarrior 17 | 18 | 4. Remove the temporary tag from the new task 19 | 20 | 5. Print the UUID to STDOUT 21 | HERE 22 | opts.banner = banner 23 | opts.version = TaskWarriorMail::VERSION 24 | end.parse! 25 | 26 | # 1. Make a temporary tag that unlikely to exist yet. 27 | # To avoid shell troubles, we have it start with a character. 28 | tag = 'tag_' + SecureRandom.hex.tr('-', '') 29 | 30 | # 2. Create the new task tagged with our temporary tag 31 | `task rc.verbose=nothing add +#{tag} #{ARGV.join(' ')}` 32 | 33 | # 3. Remember the UUID generated by TaskWarrior 34 | task_uuid = `task rc.verbose=nothing +#{tag} _uuid`.chomp 35 | 36 | # 4. Remove the temporary tag from the new task 37 | `task rc.verbose=nothing #{task_uuid} modify -#{tag}` 38 | 39 | # 5. Print the UUID to STDOUT 40 | puts task_uuid 41 | -------------------------------------------------------------------------------- /bin/twmail: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'mail' 5 | require 'optparse' 6 | require 'twmail' 7 | 8 | OptionParser.new do |opts| 9 | banner = <<~HERE 10 | #{opts.program_name} is a simple Mail Delivery Agent (MDA) that parses received mail and creates a new TaskWarrior task from the subject of the mail. 11 | 12 | USAGE 13 | 14 | Configure fetchmail to use #{opts.program_name} as MDA: 15 | 16 | # ~/.fetchmailrc 17 | mda #{opts.program_name} 18 | HERE 19 | opts.banner = banner # .wrap 20 | opts.version = TaskWarriorMail::VERSION 21 | end.parse! 22 | 23 | mail = Mail.new(ARGF.read) 24 | task_uuid = `task-uuid \"#{mail.subject}\"` 25 | body = mail.text? ? mail.body.decoded : mail.text_part.body.decoded 26 | 27 | SEPARATOR = '-- ' 28 | body = body.split(SEPARATOR)[0] if body.include?(SEPARATOR) 29 | 30 | `task '#{task_uuid}' annotate \"#{body}\"` 31 | -------------------------------------------------------------------------------- /bin/twmail-hook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'mail' 5 | require 'optparse' 6 | require 'twmail' 7 | 8 | OptionParser.new do |opts| 9 | banner = <<~HERE 10 | #{opts.program_name} is a simple Mail Delivery Agent (MDA) that that exports the parts of the received email as environment variables and calls a shell script that can make use of these variables to further process the mail. 11 | 12 | USAGE 13 | 14 | Configure fetchmail to use #{opts.program_name} as MDA: 15 | 16 | # ~/.fetchmailrc 17 | mda #{opts.program_name} 18 | 19 | HERE 20 | opts.banner = banner # .wrap 21 | opts.version = TaskWarriorMail::VERSION 22 | end.parse! 23 | 24 | mail = Mail.new(ARGF.read) 25 | 26 | # Expose mail properties as environment variables 27 | %w[date message_id from to subject body].each do |field| 28 | value = mail.send(field.to_sym) 29 | value = value.join(',') if value.respond_to?(:join) 30 | ENV["TWMAIL_#{field.upcase}"] = Shellwords.escape(value.to_s) 31 | end 32 | 33 | # The hook to be executed is read from the environment variable TWMAIL_HOOK 34 | # If none is set, this script will assume that twmail_on_new_mail is in the 35 | # $PATH and executable. 36 | cmd = ENV.fetch('TWMAIL_HOOK', 'twmail-hook') 37 | 38 | # Call hook script 39 | `#{cmd}` 40 | -------------------------------------------------------------------------------- /doc/fetchmailrc.sample: -------------------------------------------------------------------------------- 1 | poll "imap.gmail.com" proto imap port 993 2 | user "tasks@example.com" password "passw0rd" 3 | ssl 4 | mda twmail 5 | -------------------------------------------------------------------------------- /lib/extensions/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class String 4 | # http://www.java2s.com/Code/Ruby/String/WordwrappingLinesofText.htm 5 | def wrap(width=78) 6 | gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/twmail.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'twmail/version' 4 | require 'extensions/string' 5 | 6 | module TaskWarriorMail 7 | # Your code goes here... 8 | end 9 | -------------------------------------------------------------------------------- /lib/twmail/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TaskWarriorMail 4 | VERSION = '1.0.0' 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/mail_empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerab/twmail/1fde2c56b2ef0babb73ba630a9ef798cefaa4001/test/fixtures/mail_empty.txt -------------------------------------------------------------------------------- /test/fixtures/mail_multipart.txt: -------------------------------------------------------------------------------- 1 | Delivered-To: tasks@example.com 2 | Received: by 10.114.10.161 with SMTP id j1csp21288ldb; 3 | Mon, 18 Jun 2012 13:14:55 -0700 (PDT) 4 | Received: by 10.14.127.78 with SMTP id c54mr3708525eei.8.1340050495549; 5 | Mon, 18 Jun 2012 13:14:55 -0700 (PDT) 6 | Return-Path: 7 | Received: from mail-ey0-f169.google.com (mail-ey0-f169.google.com [209.85.215.169]) 8 | by mx.google.com with ESMTPS id x56si9033076eea.113.2012.06.18.13.14.55 9 | (version=TLSv1/SSLv3 cipher=OTHER); 10 | Mon, 18 Jun 2012 13:14:55 -0700 (PDT) 11 | Received-SPF: neutral (google.com: 209.85.215.169 is neither permitted nor denied by best guess record for domain of me@example.com) client-ip=209.85.215.169; 12 | Authentication-Results: mx.google.com; spf=neutral (google.com: 209.85.215.169 is neither permitted nor denied by best guess record for domain of me@example.com) smtp.mail=me@example.com 13 | Received: by mail-ey0-f169.google.com with SMTP id n1so2188326eaa.14 14 | for ; Mon, 18 Jun 2012 13:14:55 -0700 (PDT) 15 | MIME-Version: 1.0 16 | Received: by 10.14.101.144 with SMTP id b16mr3667426eeg.225.1340050495077; 17 | Mon, 18 Jun 2012 13:14:55 -0700 (PDT) 18 | Received: by 10.14.188.4 with HTTP; Mon, 18 Jun 2012 13:14:54 -0700 (PDT) 19 | In-Reply-To: <-4460535785096073142@unknownmsgid> 20 | References: <-4460535785096073142@unknownmsgid> 21 | Date: Mon, 18 Jun 2012 22:14:54 +0200 22 | Message-ID: 23 | Subject: MenTaLguY: Atomic Operations in Ruby 24 | From: "Manager, Task" 25 | To: tasks@example.com 26 | Content-Type: multipart/alternative; boundary=bcaec52159cbbd7c5604c2c4d12e 27 | 28 | --bcaec52159cbbd7c5604c2c4d12e 29 | Content-Type: text/plain; charset=ISO-8859-1 30 | 31 | http://moonbase.rydia.net/mental/blog/programming/atomic-operations-in-ruby.html 32 | 33 | --bcaec52159cbbd7c5604c2c4d12e 34 | Content-Type: text/html; charset=ISO-8859-1 35 | 36 | 38 | 39 | --bcaec52159cbbd7c5604c2c4d12e-- 40 | -------------------------------------------------------------------------------- /test/fixtures/mail_regular.txt: -------------------------------------------------------------------------------- 1 | Date: Sat, 09 Jun 2012 21:09:29 +0200 2 | From: king.crown@nigerian-lottery.com 3 | To: you@example.com 4 | Message-ID: <4fd39f695c947_827580443948558fd@example.com> 5 | Subject: Send some test mails 6 | Mime-Version: 1.0 7 | Content-Type: text/plain; 8 | charset=UTF-8 9 | Content-Transfer-Encoding: 7bit 10 | 11 | Hi there, 12 | I am writing to you in order to inform you that you have 13 | won the Nigerian Lottery. 14 | 15 | In order to claim your win, please transfer the handling 16 | fee to the following account: 17 | 18 | AMOUNT: EUR 1234,56 19 | IBAN: AT571234500234573201 20 | BIC: ATBAATWWXXX 21 | 22 | Sincerely 23 | 24 | Toast Bread 25 | King Crown 26 | Head of The Nigerian Lottery -------------------------------------------------------------------------------- /test/fixtures/mail_separator.txt: -------------------------------------------------------------------------------- 1 | Delivered-To: task@example.com 2 | Received: by 10.152.29.4 with SMTP id f4csp2763lah; 3 | Sat, 13 Jul 2013 13:33:13 -0700 (PDT) 4 | X-Received: by 10.14.177.196 with SMTP id d44mr52014105eem.35.1373747593216; 5 | Sat, 13 Jul 2013 13:33:13 -0700 (PDT) 6 | Return-Path: 7 | Received: from mail-ee0-f45.google.com (mail-ee0-f45.google.com [74.125.83.45]) 8 | by mx.google.com with ESMTPS id 47si37216650eeu.98.2013.07.13.13.33.12 9 | for 10 | (version=TLSv1 cipher=ECDHE-RSA-RC4-SHA bits=128/128); 11 | Sat, 13 Jul 2013 13:33:13 -0700 (PDT) 12 | Received-SPF: neutral (google.com: 74.125.83.45 is neither permitted nor denied by best guess record for domain of john.doe@example.com) client-ip=74.125.83.45; 13 | Authentication-Results: mx.google.com; 14 | spf=neutral (google.com: 74.125.83.45 is neither permitted nor denied by best guess record for domain of john.doe@example.com) smtp.mail=john.doe@example.com 15 | Received: by mail-ee0-f45.google.com with SMTP id c1so6851062eek.32 16 | for ; Sat, 13 Jul 2013 13:33:12 -0700 (PDT) 17 | MIME-Version: 1.0 18 | X-Received: by 10.14.183.135 with SMTP id q7mr51321537eem.97.1373747592866; 19 | Sat, 13 Jul 2013 13:33:12 -0700 (PDT) 20 | Received: by 10.14.136.203 with HTTP; Sat, 13 Jul 2013 13:33:12 -0700 (PDT) 21 | Date: Sat, 13 Jul 2013 22:33:12 +0200 22 | Message-ID: 23 | Subject: Try IFTTT / PinReadable +kindle 24 | From: "Doe, John" 25 | To: "task@example.com" 26 | Content-Type: multipart/alternative; boundary=047d7b34385e48d21704e16a89ba 27 | 28 | --047d7b34385e48d21704e16a89ba 29 | Content-Type: text/plain; charset=UTF-8 30 | Content-Transfer-Encoding: quoted-printable 31 | 32 | https://ifttt.com/recipes/20357 33 | 34 | --=20 35 | Mit freundlichen Gr=C3=BCssen 36 | 37 | John Doe 38 | 39 | --047d7b34385e48d21704e16a89ba 40 | Content-Type: text/html; charset=UTF-8 41 | Content-Transfer-Encoding: quoted-printable 42 | 43 | https://ifttt.com/recipes/20357= 44 |

--
Mit freundlichen Gr=C3=BCssen

John Doe

45 | 46 | --047d7b34385e48d21704e16a89ba-- 47 | -------------------------------------------------------------------------------- /test/fixtures/mail_with_signature.txt: -------------------------------------------------------------------------------- 1 | Date: Sat, 09 Jun 2012 21:09:29 +0200 2 | From: king.crown@nigerian-lottery.com 3 | To: you@example.com 4 | Message-ID: <4fd39f695c947_827580443948558fd@example.com> 5 | Subject: Send some test mails 6 | Mime-Version: 1.0 7 | Content-Type: text/plain; 8 | charset=UTF-8 9 | Content-Transfer-Encoding: 7bit 10 | 11 | Hi there, 12 | I am writing to you in order to inform you that you have 13 | won the Nigerian Lottery. 14 | 15 | In order to claim your win, please transfer the handling 16 | fee to the following account: 17 | 18 | AMOUNT: EUR 1234,56 19 | IBAN: AT571234500234573201 20 | BIC: ATBAATWWXXX 21 | 22 | Sincerely 23 | 24 | Toast Bread 25 | King Crown 26 | Head of The Nigerian Lottery 27 | 28 | -- 29 | Foo 30 | -------------------------------------------------------------------------------- /test/fixtures/task01.json: -------------------------------------------------------------------------------- 1 | {"id":1,"description":"Import standard JSON with annotation","entry":"20120610T090557Z","status":"pending","uuid":"4c0be862-1f5f-4fc5-bd25-8a704c1207d2","annotations":[{"entry":"20120610T090617Z","description":"And here is somemulti-line annotation"}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/task02.json: -------------------------------------------------------------------------------- 1 | {"description":"Import minimal JSON","annotations":[{"entry":"20120610T090617Z","description":"And here is some annotation"}]} -------------------------------------------------------------------------------- /test/helpers/assertions: -------------------------------------------------------------------------------- 1 | function assert_equal() 2 | { 3 | diff <(echo "$2" ) <(echo "$1") || [ "$?" -eq 0 ] || exit 1 4 | } -------------------------------------------------------------------------------- /test/helpers/test_hook: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # twmail hook script that is called for every mail. 4 | # 5 | # The environment variables available to a hook are: 6 | # 7 | # TWMAIL_DATE 8 | # TWMAIL_MESSAGE_ID 9 | # TWMAIL_FROM 10 | # TWMAIL_TO 11 | # TWMAIL_SUBJECT 12 | # TWMAIL_BODY 13 | # 14 | # This script assumes that task-uuid is in the path. 15 | # 16 | task $(task-uuid $TWMAIL_SUBJECT) annotate $TWMAIL_BODY > /dev/null 17 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'twtest' 4 | require 'test/unit' 5 | 6 | module TaskWarriorMailTest 7 | end 8 | -------------------------------------------------------------------------------- /test/unit/test_mail.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'mail' 5 | 6 | class TestMail < Test::Unit::TestCase 7 | def test_signature 8 | m = Mail.new(File.read(fixture('mail_with_signature.txt'))) 9 | assert(m) 10 | assert(m.body.include?('-- ')) 11 | assert_false(m.body.decoded.split('-- ').include?('-- ')) 12 | end 13 | 14 | def fixture(name) 15 | File.join(File.dirname(__FILE__), '..', 'fixtures', name) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/unit/test_task_uuid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'shellwords' 4 | require 'open3' 5 | require 'test/unit' 6 | 7 | class Command 8 | def exec(cmd='', args={}) 9 | env.each { |k, v| ENV[k.to_s] = v.to_s } 10 | line = build_line(cmd, args) 11 | Open3.capture3(line) 12 | end 13 | 14 | def env 15 | {} 16 | end 17 | 18 | def default_args 19 | {} 20 | end 21 | 22 | def executable 23 | raise 'Subclasses must override this method' 24 | end 25 | 26 | private 27 | 28 | def build_line(cmd='', args={}) 29 | [].tap { |line| 30 | line << executable 31 | line << default_args.merge(args).map { |k, v| "#{Shellwords.escape(k.strip)}=#{Shellwords.escape(v.strip)}" }.join(' ') 32 | line << cmd.strip 33 | line.reject!(&:empty?) 34 | }.join(' ') 35 | end 36 | 37 | def overrides(env) 38 | intersection = env.keys.to_set & ENV.keys.to_set 39 | ENV.select { |k, v| intersection.include?(k) } 40 | end 41 | end 42 | 43 | class TaskWarriorCommand < Command 44 | attr_accessor :data_dir 45 | 46 | def version 47 | exec('_version') 48 | end 49 | 50 | def count 51 | exec('count') 52 | end 53 | 54 | def env 55 | raise "data_dir must not be empty for '#{executable}'" if @data_dir.nil? || data_dir.empty? 56 | { TASKDATA: @data_dir } 57 | end 58 | 59 | def default_args 60 | { 'rc.verbose' => 'nothing', 'rc.json.array' => 'on' } 61 | end 62 | 63 | def executable 64 | 'task' 65 | end 66 | end 67 | 68 | class TaskUUID < TaskWarriorCommand 69 | def create(description) 70 | exec(description) 71 | end 72 | 73 | def executable 74 | # The gem version that uses the binstub fails if the script is not Ruby 75 | # 'task-uuid' 76 | 77 | # The local version, called directly, works fine even if it's a Bash script 78 | File.join(File.dirname(__FILE__), '..', '..', 'bin', 'task-uuid') 79 | end 80 | 81 | def default_args 82 | {} 83 | end 84 | end 85 | 86 | class TestTaskUUID < Test::Unit::TestCase 87 | def setup 88 | @tw = TaskWarriorCommand.new 89 | @tw.data_dir = Dir.mktmpdir(name) 90 | 91 | raise "TASKRC must not be set, but it is #{ENV['TASKRC']}" if ENV['TASKRC'] 92 | end 93 | 94 | def teardown 95 | FileUtils.rm_rf(@tw.data_dir) 96 | end 97 | 98 | def test_version 99 | out, err, status = @tw.version 100 | assert(status.success?) 101 | assert_empty(err) 102 | assert_not_empty(out) 103 | assert_match(/\d\.\d\.\d/, out.chomp) 104 | end 105 | 106 | def test_empty 107 | out, err, status = @tw.count 108 | assert(status.success?) 109 | assert_empty(err) 110 | assert_not_empty(out) 111 | assert_equal('0', out.chomp) 112 | end 113 | 114 | def test_task_uuid 115 | task_uuid = TaskUUID.new 116 | task_uuid.data_dir = Dir.mktmpdir(name) 117 | out, err, _ = task_uuid.create('foo bar') 118 | assert_empty(err) 119 | assert_not_empty(out) 120 | assert_match(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/, out.chomp) 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/unit/test_twmail.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class TestHelpers < TaskWarrior::Test::Integration::Test 6 | def teardown 7 | ENV['TASKRC'] = nil 8 | super 9 | end 10 | 11 | def test_regular 12 | result = deliver_fixture(0, fixture('mail_regular.txt')) 13 | assert_empty(result) 14 | assert_equal(1, task('count').to_i) 15 | 16 | tasks = export_tasks 17 | assert_equal(1, tasks.size) 18 | assert_equal('Send some test mails', tasks.first['description']) 19 | 20 | assert_equal(1, tasks.first['annotations'].size) 21 | assert(tasks.first['annotations'].first.to_s =~ /Nigeria/) 22 | end 23 | 24 | def test_regular_with_signature 25 | result = deliver_fixture(0, fixture('mail_with_signature.txt')) 26 | assert_empty(result) 27 | assert_equal(1, task('count').to_i) 28 | 29 | tasks = export_tasks 30 | assert_equal(1, tasks.size) 31 | assert_equal('Send some test mails', tasks.first['description']) 32 | 33 | assert_equal(1, tasks.first['annotations'].size) 34 | assert(tasks.first['annotations'].first.to_s =~ /Nigeria/) 35 | end 36 | 37 | def test_multipart 38 | result = deliver_fixture(0, fixture('mail_multipart.txt')) 39 | assert_empty(result) 40 | assert_equal(1, task('count').to_i) 41 | 42 | tasks = export_tasks 43 | assert_equal(1, tasks.size) 44 | assert_equal('MenTaLguY: Atomic Operations in Ruby', tasks.first['description']) 45 | 46 | assert_equal(1, tasks.first['annotations'].size) 47 | assert(tasks.first['annotations'].first.to_s =~ /atomic/) 48 | end 49 | 50 | def test_separator 51 | result = deliver_fixture(0, fixture('mail_separator.txt')) 52 | assert_empty(result) 53 | assert_equal(1, task('count').to_i) 54 | 55 | tasks = export_tasks 56 | assert_equal(1, tasks.size) 57 | task = tasks.first 58 | assert_equal('Try IFTTT / PinReadable', task['description']) 59 | assert_true(task['tags'].include?('kindle')) 60 | end 61 | 62 | def test_missing_fixture 63 | result = deliver_fixture(1, 'missing fixture') 64 | assert_empty(result) 65 | end 66 | 67 | protected 68 | 69 | def deliver_fixture(status, fixture) 70 | twmail = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'twmail') 71 | ENV['TASKRC'] = @taskrc_file 72 | output = `cat #{fixture} | #{twmail}` 73 | assert_equal(status, $CHILD_STATUS.exitstatus) 74 | output 75 | end 76 | 77 | def fixture(name) 78 | File.join(File.dirname(__FILE__), '..', 'fixtures', name) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/unit/test_twmail_hooks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Unit test for twmail-hook 5 | # 6 | 7 | # Data locations 8 | DIRNAME=$(cd $(dirname $0);pwd) 9 | DATA_DIR=$(mktemp -dt $(basename $0)) 10 | export TASKRC=$(mktemp -t taskrc) 11 | 12 | source $DIRNAME/../helpers/assertions 13 | 14 | # Create custom taskrc 15 | cat > $TASKRC < 1.2.0' 24 | gem.add_development_dependency 'guard-test' 25 | gem.add_development_dependency 'guard-bundler' 26 | gem.add_development_dependency 'pry' 27 | gem.add_development_dependency 'rake' 28 | gem.add_development_dependency 'rubocop' 29 | end 30 | --------------------------------------------------------------------------------