replyr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/Gemfile +14 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +94 -0
  6. data/Rakefile +32 -0
  7. data/lib/email_reply_parser/LICENSE +22 -0
  8. data/lib/email_reply_parser/email_reply_parser.rb +475 -0
  9. data/lib/generators/replyr/install_generator.rb +15 -0
  10. data/lib/generators/replyr/templates/mailman_daemon +12 -0
  11. data/lib/generators/replyr/templates/mailman_server +50 -0
  12. data/lib/generators/replyr/templates/replyr.rb.erb +8 -0
  13. data/lib/replyr/config.rb +22 -0
  14. data/lib/replyr/engine.rb +14 -0
  15. data/lib/replyr/handle_reply.rb +36 -0
  16. data/lib/replyr/reply_address.rb +111 -0
  17. data/lib/replyr/reply_email.rb +50 -0
  18. data/lib/replyr/version.rb +3 -0
  19. data/lib/replyr.rb +26 -0
  20. data/lib/tasks/replyr_tasks.rake +4 -0
  21. data/test/dummy/README.rdoc +28 -0
  22. data/test/dummy/Rakefile +6 -0
  23. data/test/dummy/app/assets/javascripts/application.js +13 -0
  24. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  25. data/test/dummy/app/controllers/application_controller.rb +5 -0
  26. data/test/dummy/app/helpers/application_helper.rb +2 -0
  27. data/test/dummy/app/models/comment.rb +7 -0
  28. data/test/dummy/app/models/user.rb +3 -0
  29. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  30. data/test/dummy/bin/bundle +3 -0
  31. data/test/dummy/bin/rails +4 -0
  32. data/test/dummy/bin/rake +4 -0
  33. data/test/dummy/config/application.rb +23 -0
  34. data/test/dummy/config/boot.rb +5 -0
  35. data/test/dummy/config/database.yml +8 -0
  36. data/test/dummy/config/environment.rb +5 -0
  37. data/test/dummy/config/environments/development.rb +29 -0
  38. data/test/dummy/config/environments/production.rb +80 -0
  39. data/test/dummy/config/environments/test.rb +36 -0
  40. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  41. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  42. data/test/dummy/config/initializers/inflections.rb +16 -0
  43. data/test/dummy/config/initializers/mime_types.rb +5 -0
  44. data/test/dummy/config/initializers/replyr.rb +2 -0
  45. data/test/dummy/config/initializers/secret_token.rb +12 -0
  46. data/test/dummy/config/initializers/session_store.rb +3 -0
  47. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  48. data/test/dummy/config/locales/en.yml +23 -0
  49. data/test/dummy/config/routes.rb +56 -0
  50. data/test/dummy/config.ru +4 -0
  51. data/test/dummy/db/test.sqlite3 +0 -0
  52. data/test/dummy/log/development.log +0 -0
  53. data/test/dummy/log/test.log +12557 -0
  54. data/test/dummy/public/404.html +58 -0
  55. data/test/dummy/public/422.html +58 -0
  56. data/test/dummy/public/500.html +57 -0
  57. data/test/dummy/public/favicon.ico +0 -0
  58. data/test/email_reply_parser/email_reply_parser_test.rb +488 -0
  59. data/test/email_reply_parser/emails/correct_sig.txt +4 -0
  60. data/test/email_reply_parser/emails/email_1_1.txt +13 -0
  61. data/test/email_reply_parser/emails/email_1_2.txt +51 -0
  62. data/test/email_reply_parser/emails/email_1_3.txt +55 -0
  63. data/test/email_reply_parser/emails/email_1_4.txt +5 -0
  64. data/test/email_reply_parser/emails/email_1_5.txt +15 -0
  65. data/test/email_reply_parser/emails/email_1_6.txt +15 -0
  66. data/test/email_reply_parser/emails/email_1_7.txt +12 -0
  67. data/test/email_reply_parser/emails/email_1_8.txt +6 -0
  68. data/test/email_reply_parser/emails/email_1_9.txt +9 -0
  69. data/test/email_reply_parser/emails/email_2_1.txt +25 -0
  70. data/test/email_reply_parser/emails/email_2_2.txt +10 -0
  71. data/test/email_reply_parser/emails/email_2_3.txt +14 -0
  72. data/test/email_reply_parser/emails/email_2_4.txt +14 -0
  73. data/test/email_reply_parser/emails/email_2_5.txt +15 -0
  74. data/test/email_reply_parser/emails/email_2_6.txt +11 -0
  75. data/test/email_reply_parser/emails/email_2_7.txt +5 -0
  76. data/test/email_reply_parser/emails/email_2_7_1.txt +5 -0
  77. data/test/email_reply_parser/emails/email_2_7_de.txt +5 -0
  78. data/test/email_reply_parser/emails/email_2_8.txt +3 -0
  79. data/test/email_reply_parser/emails/email_2_9.txt +9 -0
  80. data/test/email_reply_parser/emails/email_2nd_paragraph_starting_with_on.txt +12 -0
  81. data/test/email_reply_parser/emails/email_BlackBerry.txt +3 -0
  82. data/test/email_reply_parser/emails/email_bullets.txt +22 -0
  83. data/test/email_reply_parser/emails/email_from_address_in_quote_header.txt +12 -0
  84. data/test/email_reply_parser/emails/email_from_name_in_quote_header.txt +12 -0
  85. data/test/email_reply_parser/emails/email_hyphens.txt +5 -0
  86. data/test/email_reply_parser/emails/email_iPhone.txt +3 -0
  87. data/test/email_reply_parser/emails/email_iPhone_de.txt +3 -0
  88. data/test/email_reply_parser/emails/email_iPhone_de_2.txt +3 -0
  89. data/test/email_reply_parser/emails/email_mentions_own_email_address.txt +6 -0
  90. data/test/email_reply_parser/emails/email_mentions_own_name.txt +6 -0
  91. data/test/email_reply_parser/emails/email_multi_word_sent_from_my_mobile_device.txt +3 -0
  92. data/test/email_reply_parser/emails/email_multiline_quote_header_de_mx.txt +8 -0
  93. data/test/email_reply_parser/emails/email_multiline_quote_header_es_mx.txt +8 -0
  94. data/test/email_reply_parser/emails/email_multiline_quote_header_fr.txt +8 -0
  95. data/test/email_reply_parser/emails/email_multiline_quote_header_from_first.txt +11 -0
  96. data/test/email_reply_parser/emails/email_multiline_quote_header_from_replyto_date_to_subject.txt +12 -0
  97. data/test/email_reply_parser/emails/email_multiline_quote_header_from_to_date_subject.txt +11 -0
  98. data/test/email_reply_parser/emails/email_multiline_quote_header_none.txt +11 -0
  99. data/test/email_reply_parser/emails/email_multiline_quote_header_pt_br.txt +8 -0
  100. data/test/email_reply_parser/emails/email_multiline_quote_header_with_asterisks.txt +21 -0
  101. data/test/email_reply_parser/emails/email_multiline_quote_header_with_cc.txt +9 -0
  102. data/test/email_reply_parser/emails/email_multiline_quote_header_with_multiline_headers.txt +14 -0
  103. data/test/email_reply_parser/emails/email_no_signature_deliminator.txt +7 -0
  104. data/test/email_reply_parser/emails/email_no_signature_deliminator_adds_a_middle_initial.txt +7 -0
  105. data/test/email_reply_parser/emails/email_one_is_not_on.txt +10 -0
  106. data/test/email_reply_parser/emails/email_sent_from_my_not_signature.txt +3 -0
  107. data/test/email_reply_parser/emails/email_was_showing_as_nothing_visible.txt +13 -0
  108. data/test/email_reply_parser/emails/new_content/email_1_2.txt +28 -0
  109. data/test/replyr/handle_reply_test.rb +46 -0
  110. data/test/replyr/reply_address_test.rb +77 -0
  111. data/test/replyr/reply_email_test.rb +19 -0
  112. data/test/replyr_test.rb +7 -0
  113. data/test/support/emails/reply_multipart.eml +35 -0
  114. data/test/support/emails/reply_plain.eml +11 -0
  115. data/test/support/emails/reply_with_attachment.eml +466 -0
  116. data/test/test_helper.rb +23 -0
  117. metadata +228 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 05a40f3b1f0148dcff5d4643db55547ab76c7b1e
4
+ data.tar.gz: bad9440558e05ca2e926517dda94d662339e02a7
5
+ SHA512:
6
+ metadata.gz: 43442dddff869b36e478e8e415f2d680ca64a9d5a0e8f45ff2303ee4739684c66a2865e28069785964c901a1ea1596f09c5623259a30dacc5a3336dbf2e120b9
7
+ data.tar.gz: a62949cae32390d4639d08eb3836002ade91b9674555e4187f21cba87224d8350bd3cade18425b82c87ea9360592f999d5767ff137f4f9f5572a53261ba58892
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## v0.0.1
2
+
3
+ initial release
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Declare your gem's dependencies in replyr.gemspec.
4
+ # Bundler will treat runtime dependencies like base dependencies, and
5
+ # development dependencies will be added by default to the :development group.
6
+ gemspec
7
+
8
+ # Declare any dependencies that are still in development here instead of in
9
+ # your gemspec. These might include edge Rails or gems from your path or
10
+ # Git. Remember to move these dependencies to your gemspec before releasing
11
+ # your gem to rubygems.org.
12
+
13
+ # To use debugger
14
+ # gem 'debugger'
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 Philipp Wüllner
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ #Replyr [![Build Status](https://travis-ci.org/wursttheke/replyr.png?branch=master)](https://travis-ci.org/wursttheke/replyr)
2
+
3
+ Replyr lets you receive and process reply emails with Rails. And with ease!
4
+
5
+ #### Here's an example
6
+
7
+ > A User gets an email notification about a comment being posted on his page. Now he answers this email directly from his email client with a followup comment. Replyr receives the anwser and creates a new comment on his page.
8
+
9
+ #### How does this work?
10
+
11
+ Replyr generates a special `reply_to` email address which is unique to the user receiving the notification and the object in question (e.g. a comment). The resulting reply email address looks something like this:
12
+
13
+ reply-comment-12-56-01ce26dc69094af9246ea7e7ce9970aff2b81cc9@example.com
14
+
15
+ This address will be set as the reply_to address in the outgoing email from the rails app. When the user answers the email, the message will be sent to this address and Replyr will be able to handle it accordingly. It will check the mail for validity and then pass it to your custom code, which will then create a new comment.
16
+
17
+ #### How do the emails get into my app?
18
+
19
+ Replyr uses the awesome [Mailman Gem](https://github.com/titanous/mailman) to receive the reply emails. It can be configured to get the emails via Maildir, from a POP3- or from an IMAP-Server. It runs as a background daemon and observes your email directory or polls your email server for new emails. Take a look at the documentation to find out about its features and how to set it up:
20
+
21
+ https://github.com/titanous/mailman
22
+
23
+ ## Requirements
24
+
25
+ - Ruby >= 1.9.3
26
+ - Rails 3 >= 3.1 or Rails 4.x
27
+
28
+ ## Installtion
29
+
30
+ #### Add the gem to your `Gemfile`
31
+
32
+ ```ruby
33
+ gem 'replyr'
34
+ ```
35
+
36
+ #### Install
37
+
38
+ ```bash
39
+ $ bundle
40
+ $ rails g replyr:install
41
+ ```
42
+
43
+ #### Edit initializer
44
+
45
+ Open up `config/initializers/replyr.rb` and set the host name of your reply email address.
46
+
47
+ ```ruby
48
+ Ryplr.config.host = "yourdomain.com"
49
+ ```
50
+
51
+ #### Setup Mailman Gem
52
+
53
+ The Install Generator will already have created a `script/mailman_server` file which boots up the Mailman background job. According to your setup you will have to configure the file to your needs. By default it is setup to observe the `~/Maildir` folder in your home directory.
54
+
55
+ ## Usage
56
+
57
+ #### Make a model accept replies
58
+
59
+ Update your ActiveRecord models you want to reply to by adding a `handle_reply` like this:
60
+
61
+ ```ruby
62
+ class Comment < ActiveRecord::Base
63
+ belongs_to :user
64
+
65
+ handle_reply do |comment, user, text, files|
66
+ Comment.create(body: text)
67
+ end
68
+ end
69
+ ```
70
+
71
+ The `handle_reply` method takes a block with 4 parameters. The first one is the original object (in this case the comment). The second is the user who is sending the reply. The third is the reply plain text from the email (HTML mails are not supported). The fourth and last argument is an Array of `StringIO` objects representing all files attached to the emails.
72
+
73
+ What you put in the `handle_reply` block is completely up to you. In this example we are just creating a new comment, but you could also handle attached files by passing them to carrierwave, etc.
74
+
75
+ #### Make your mailers send the `reply_to` address
76
+
77
+ To add the unique reply address to your outgoing emails and make them 'replyable', add the reply_to option to your mailers methods:
78
+
79
+ ```ruby
80
+ class CommentMailer < ActionMailer::Base
81
+ def new_comment(user, comment)
82
+ mail(to: user.email, reply_to: comment.reply_address_for_user(user))
83
+ end
84
+ end
85
+ ```
86
+
87
+ ## Start up
88
+
89
+ Start/Stop up the Mailman background worker with the following commands:
90
+
91
+ ```bash
92
+ $ RAILS_ENV=production script/mailman_daemon start
93
+ $ RAILS_ENV=production script/mailman_daemon stop
94
+ ```
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Replyr'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rake/testtask'
23
+
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << 'lib'
26
+ t.libs << 'test'
27
+ t.pattern = 'test/**/*_test.rb'
28
+ t.verbose = false
29
+ end
30
+
31
+
32
+ task default: :test
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) GitHub
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
13
+ all 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
21
+ THE SOFTWARE.
22
+
@@ -0,0 +1,475 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'strscan'
3
+
4
+ # EmailReplyParser is a small library to parse plain text email content. The
5
+ # goal is to identify which fragments are quoted, part of a signature, or
6
+ # original body content. We want to support both top and bottom posters, so
7
+ # no simple "REPLY ABOVE HERE" content is used.
8
+ #
9
+ # Beyond RFC 5322 (which is handled by the [Ruby mail gem][mail]), there aren't
10
+ # any real standards for how emails are created. This attempts to parse out
11
+ # common conventions for things like replies:
12
+ #
13
+ # this is some text
14
+ #
15
+ # On <date>, <author> wrote:
16
+ # > blah blah
17
+ # > blah blah
18
+ #
19
+ # ... and signatures:
20
+ #
21
+ # this is some text
22
+ #
23
+ # --
24
+ # Bob
25
+ # http://homepage.com/~bob
26
+ #
27
+ # Each of these are parsed into Fragment objects.
28
+ #
29
+ # EmailReplyParser also attempts to figure out which of these blocks should
30
+ # be hidden from users.
31
+ #
32
+ class EmailReplyParser
33
+ # Public: Splits an email body into a list of Fragments.
34
+ #
35
+ # text - A String email body.
36
+ # from_address - from address of the email (optional)
37
+ #
38
+ # Returns an Email instance.
39
+ def self.read(text, from_address = "")
40
+ Email.new.read(text, from_address)
41
+ end
42
+
43
+ # Public: Get the text of the visible portions of the given email body.
44
+ #
45
+ # text - A String email body.
46
+ # from_address - from address of the email (optional)
47
+ #
48
+ # Returns a String.
49
+ def self.parse_reply(text, from_address = "")
50
+ self.read(text.to_s, from_address).visible_text
51
+ end
52
+
53
+ def self.parse_new_content(text, from_address = "")
54
+ self.read(text, from_address).new_content
55
+ end
56
+
57
+ ### Emails
58
+
59
+ # An Email instance represents a parsed body String.
60
+ class Email
61
+ # Emails have an Array of Fragments.
62
+ attr_reader :fragments
63
+
64
+ def initialize
65
+ @fragments = []
66
+ end
67
+
68
+ # Public: Gets the combined text of the visible fragments of the email body.
69
+ #
70
+ # Returns a String.
71
+ def visible_text
72
+ fragments.select{|f| !f.hidden?}.map{|f| f.to_s}.join("\n").rstrip
73
+ end
74
+
75
+ def new_content
76
+ fragments.select{|f| !f.quoted? && !f.hidden? && !f.signature?}.map{|f| f.to_s}.join("\n").rstrip
77
+ end
78
+
79
+ # Splits the given text into a list of Fragments. This is roughly done by
80
+ # reversing the text and parsing from the bottom to the top. This way we
81
+ # can check for 'On <date>, <author> wrote:' lines above quoted blocks.
82
+ #
83
+ # text - A String email body.
84
+ # from_address - from address of the email (optional)
85
+ #
86
+ # Returns this same Email instance.
87
+ def read(text, from_address = "")
88
+ # parse out the from name if one exists and save for use later
89
+ @from_name_raw = parse_raw_name_from_address(from_address)
90
+ @from_name_normalized = normalize_name(@from_name_raw)
91
+ @from_email = parse_email_from_address(from_address)
92
+
93
+ text = normalize_text(text)
94
+
95
+ # The text is reversed initially due to the way we check for hidden
96
+ # fragments.
97
+ text = text.reverse
98
+
99
+ # This determines if any 'visible' Fragment has been found. Once any
100
+ # visible Fragment is found, stop looking for hidden ones.
101
+ @found_visible = false
102
+
103
+ # This instance variable points to the current Fragment. If the matched
104
+ # line fits, it should be added to this Fragment. Otherwise, finish it
105
+ # and start a new Fragment.
106
+ @fragment = nil
107
+
108
+ # Use the StringScanner to pull out each line of the email content.
109
+ @scanner = StringScanner.new(text)
110
+ while line = @scanner.scan_until(/\n/n)
111
+ scan_line(line)
112
+ end
113
+
114
+ # Be sure to parse the last line of the email.
115
+ if (last_line = @scanner.rest.to_s).size > 0
116
+ scan_line(last_line, true)
117
+ end
118
+
119
+ # Finish up the final fragment. Finishing a fragment will detect any
120
+ # attributes (hidden, signature, reply), and join each line into a
121
+ # string.
122
+ finish_fragment
123
+
124
+ @scanner = @fragment = nil
125
+
126
+ # Now that parsing is done, reverse the order.
127
+ @fragments.reverse!
128
+ self
129
+ end
130
+
131
+ private
132
+ EMPTY = "".freeze
133
+
134
+ COMMON_REPLY_HEADER_REGEXES = [
135
+ /^On(.+)wrote:$/nm,
136
+ /^Am(.+)schrieb(.+):$/nm,
137
+ /^Le(.+)a(.+)crit( )?:$/nm,
138
+ /\A\d{4}\/\d{1,2}\/\d{1,2}\s+.{1,80}\s<[^@]+@[^@]+>\Z/,
139
+ ]
140
+
141
+ # Line optionally starts with whitespace, contains two or more hyphens or
142
+ # underscores, and ends with optional whitespace.
143
+ # Example: '---' or '___' or '--- '
144
+ MULTI_LINE_SIGNATURE_REGEX = /^\s*[-_]{2,}\s*$/
145
+
146
+ # Line optionally starts with whitespace, followed by one hyphen, followed by a word character
147
+ # Example: '-Sandro'
148
+ ONE_LINE_SIGNATURE_REGEX = /^\s*-\w/
149
+
150
+ ORIGINAL_MESSAGE_SIGNATURE_REGEXES = [
151
+ /^[\s_-]+(Original Message)?[\s_-]+$/,
152
+ /^[\s_-]+(Original Nachricht)?[\s_-]+$/,
153
+ /^[\s_-]+(Reply Message)?[\s_-]+$/,
154
+ /^[\s_-](.+)(ngliche Nachricht)[\s_-]+$/,
155
+ ]
156
+
157
+ # No block-quotes (> or <), followed by up to three words, followed by "Sent from my".
158
+ # Example: "Sent from my iPhone 3G"
159
+ SENT_FROM_REGEXES = [
160
+ /^Sent from my (\s*\w+){1,3}(\s*<.*>)?$/,
161
+ /^Von (\s*\w+){1,3}(\s*<.*>)? gesendet$/,
162
+ /^Gesendet von meinem (\s*\w+){1,3}(\s*<.*>)?$/,
163
+ /^Gesendet mit meinem (\s*\w+){1,3}(\s*<.*>)?$/,
164
+ ]
165
+
166
+ if defined?(Regexp::NOENCODING)
167
+ SIGNATURE_REGEX = Regexp.new(Regexp.union(MULTI_LINE_SIGNATURE_REGEX, ONE_LINE_SIGNATURE_REGEX, Regexp.union(ORIGINAL_MESSAGE_SIGNATURE_REGEXES), Regexp.union(SENT_FROM_REGEXES)).source, Regexp::NOENCODING)
168
+ else
169
+ SIGNATURE_REGEX = Regexp.new(Regexp.union(MULTI_LINE_SIGNATURE_REGEX, ONE_LINE_SIGNATURE_REGEX, Regexp.union(ORIGINAL_MESSAGE_SIGNATURE_REGEXES), Regexp.union(SENT_FROM_REGEXES)).source)
170
+ end
171
+
172
+ # TODO: refactor out in a i18n.yml file
173
+ # Supports English, French, Es-Mexican, Pt-Brazilian
174
+ # Maps a label to a label-group
175
+ QUOTE_HEADER_LABELS = Hash[*{
176
+ :from => ["From", "De", "Von"],
177
+ :to => ["To", "Para", "A", "An"],
178
+ :cc => ["CC"],
179
+ :reply_to => ["Reply-To"],
180
+ :date => ["Date", "Sent", "Gesendet", "Enviado", "Enviada em", "Fecha", "Datum"],
181
+ :subject => ["Subject", "Assunto", "Asunto", "Objet", "Betreff"]
182
+ }.map {|group, labels| labels.map {|label| [label.downcase, group]}}.flatten]
183
+
184
+ # normalize text so it is easier to parse
185
+ #
186
+ # text - text to normalize
187
+ #
188
+ # Returns a String
189
+ def normalize_text(text)
190
+ # in 1.9 we want to operate on the raw bytes
191
+ text = text.dup.force_encoding('binary') if text.respond_to?(:force_encoding)
192
+
193
+ # Normalize line endings.
194
+ text.gsub!("\r\n", "\n")
195
+
196
+ # Check for multi-line reply headers. Some clients break up
197
+ # the "On DATE, NAME <EMAIL> wrote:" line into multiple lines.
198
+ if match = text.match(/^(On\s(.+)wrote:)$/m)
199
+ # Remove all new lines from the reply header. as long as we don't have any double newline
200
+ # if we do they we have grabbed something that is not actually a reply header
201
+ text.gsub! match[1], match[1].gsub("\n", " ") unless match[1] =~ /\n\n/
202
+ end
203
+
204
+ # Some users may reply directly above a line of underscores or dashes.
205
+ # In order to ensure that these fragments are split correctly,
206
+ # make sure that all lines of underscores are preceded by
207
+ # at least two newline characters.
208
+ text.gsub!(/([^\n])(?=\n_{7}_+)$/m, "\\1\n")
209
+ text.gsub!(/([^\n])(?=\n-+)$/m, "\\1\n")
210
+
211
+ text
212
+ end
213
+
214
+ # Parse a person's name from an e-mail address
215
+ #
216
+ # email - email address.
217
+ #
218
+ # Returns a String.
219
+ def parse_name_from_address(address)
220
+ normalize_name(parse_raw_name_from_address(address))
221
+ end
222
+
223
+ def parse_raw_name_from_address(address)
224
+ match = address.match(/^["']*([\w\s,]+)["']*\s*</)
225
+ match ? match[1].strip.to_s : EMPTY
226
+ end
227
+
228
+ def parse_email_from_address(address)
229
+ match = address.match /<(.*)>/
230
+ match ? match[1] : address
231
+ end
232
+
233
+ # Normalize a name to First Last
234
+ #
235
+ # name - name to normailze.
236
+ #
237
+ # Returns a String.
238
+ def normalize_name(name)
239
+ if name.include?(',')
240
+ make_name_first_then_last(name)
241
+ else
242
+ name
243
+ end
244
+ end
245
+
246
+ def make_name_first_then_last(name)
247
+ split_name = name.split(',')
248
+ if split_name[0].include?(" ")
249
+ split_name[0].to_s
250
+ else
251
+ split_name[1].strip + " " + split_name[0].strip
252
+ end
253
+ end
254
+
255
+ ### Line-by-Line Parsing
256
+
257
+ # Scans the given line of text and determines which fragment it belongs to.
258
+ def scan_line(line, last = false)
259
+ line.chomp!("\n")
260
+ line.reverse!
261
+ line.rstrip!
262
+
263
+ # Mark the current Fragment as a signature if the current line is empty
264
+ # and the Fragment starts with a common signature indicator.
265
+ # Mark the current Fragment as a quote if the current line is empty
266
+ # and the Fragment starts with a multiline quote header.
267
+ scan_signature_or_quote if @fragment && line == EMPTY
268
+
269
+ # We're looking for leading `>`'s to see if this line is part of a
270
+ # quoted Fragment.
271
+ is_quoted = !!(line =~ /^>+/n)
272
+
273
+ # Note that a common reply header also counts as part of the quoted
274
+ # Fragment, even though it doesn't start with `>`.
275
+ unless @fragment &&
276
+ ((@fragment.quoted? == is_quoted) ||
277
+ (@fragment.quoted? && (line_is_reply_header?(line) || line == EMPTY)))
278
+ finish_fragment
279
+ @fragment = Fragment.new
280
+ @fragment.quoted = is_quoted
281
+ end
282
+
283
+ @fragment.add_line(line)
284
+ scan_signature_or_quote if last
285
+ end
286
+
287
+ def scan_signature_or_quote
288
+ if signature_line?(@fragment.lines.first)
289
+ @fragment.signature = true
290
+ finish_fragment
291
+ elsif multiline_quote_header_in_fragment?
292
+ @fragment.quoted = true
293
+ finish_fragment
294
+ end
295
+ end
296
+
297
+ # Returns +true+ if the current block in the current fragment has
298
+ # a multiline quote header, +false+ otherwise.
299
+ #
300
+ # The quote header we're looking for is mainly generated by Outlook
301
+ # clients. It's considered a quote header if the first 4 folded lines
302
+ # have one of the following forms:
303
+ #
304
+ # label: some text
305
+ # *label:* some text
306
+ #
307
+ # where a line like this:
308
+ #
309
+ # label: some text
310
+ # possibly indented text that belongs to the previous line
311
+ #
312
+ # is folded into:
313
+ #
314
+ # label: some text possibly indented text that belongs to the previous line
315
+ #
316
+ # and where label is a value from +QUOTE_HEADER_LABELS+ that appears
317
+ # only once in the first 4 lines and where each group of a label
318
+ # is represented at most once.
319
+ def multiline_quote_header_in_fragment?
320
+ folding = false
321
+ label_groups = []
322
+ @fragment.current_block.split("\n").each do |line|
323
+ if line =~ /\A\s*\*?([^:]+):(\s|\*)/
324
+ label = QUOTE_HEADER_LABELS[$1.downcase]
325
+ if label
326
+ return false if label_groups.include?(label)
327
+ return true if label_groups.length == 3
328
+ label_groups << label
329
+ folding = true
330
+ elsif !folding
331
+ return false
332
+ end
333
+ elsif !folding
334
+ return false
335
+ else
336
+ folding = true
337
+ end
338
+ end
339
+ return false
340
+ end
341
+
342
+ # Detects if a given line is the beginning of a signature
343
+ #
344
+ # line - A String line of text from the email.
345
+ #
346
+ # Returns true if the line is the beginning of a signature, or false.
347
+ def signature_line?(line)
348
+ line =~ SIGNATURE_REGEX || line_is_signature_name?(line)
349
+ end
350
+
351
+ # Detects if a given line is a common reply header.
352
+ #
353
+ # line - A String line of text from the email.
354
+ #
355
+ # Returns true if the line is a valid header, or false.
356
+ def line_is_reply_header?(line)
357
+ COMMON_REPLY_HEADER_REGEXES.each do |regex|
358
+ return true if line =~ regex
359
+ end
360
+ false
361
+ end
362
+
363
+ # Detects if the @from name is a big part of a given line and therefore the beginning of a signature
364
+ #
365
+ # line - A String line of text from the email.
366
+ #
367
+ # Returns true if @from_name is a big part of the line, or false.
368
+ def line_is_signature_name?(line)
369
+ regexp = generate_regexp_for_name()
370
+ @from_name_normalized != "" && (line =~ regexp) && ((@from_name_normalized.size.to_f / line.size) > 0.25)
371
+ end
372
+
373
+ #generates regexp which always for additional words or initials between first and last names
374
+ def generate_regexp_for_name
375
+ name_parts = @from_name_normalized.split(" ")
376
+ seperator = '[\w.\s]*'
377
+ regexp = Regexp.new(name_parts.join(seperator), Regexp::IGNORECASE)
378
+ end
379
+
380
+ # Builds the fragment string, after all lines have been added.
381
+ # It also checks to see if this Fragment is hidden. The hidden
382
+ # Fragment check reads from the bottom to the top.
383
+ #
384
+ # Any quoted Fragments or signature Fragments are marked hidden if they
385
+ # are below any visible Fragments. Visible Fragments are expected to
386
+ # contain original content by the author. If they are below a quoted
387
+ # Fragment, then the Fragment should be visible to give context to the
388
+ # reply.
389
+ #
390
+ # some original text (visible)
391
+ #
392
+ # > do you have any two's? (quoted, visible)
393
+ #
394
+ # Go fish! (visible)
395
+ #
396
+ # > --
397
+ # > Player 1 (quoted, hidden)
398
+ #
399
+ # --
400
+ # Player 2 (signature, hidden)
401
+ #
402
+ def finish_fragment
403
+ if @fragment
404
+ @fragment.finish
405
+ if !@found_visible
406
+ if @fragment.quoted? || @fragment.signature? ||
407
+ @fragment.reply_header? || @fragment.to_s.strip == EMPTY
408
+ @fragment.hidden = true
409
+ else
410
+ @found_visible = true
411
+ end
412
+ end
413
+ @fragments << @fragment
414
+ end
415
+ @fragment = nil
416
+ end
417
+ end
418
+
419
+ # Represents a group of paragraphs in the email sharing common attributes.
420
+ # Paragraphs should get their own fragment if they are a quoted area or a
421
+ # signature.
422
+ class Fragment < Struct.new(:quoted, :signature, :reply_header, :hidden)
423
+ # Array of string lines that make up the content of this fragment.
424
+ attr_reader :lines
425
+
426
+ # Array of string lines that is being processed not having
427
+ # an empty line.
428
+ attr_reader :current_block
429
+
430
+ # This is reserved for the joined String that is build when this Fragment
431
+ # is finished.
432
+ attr_reader :content
433
+
434
+ def initialize
435
+ self.quoted = self.signature = self.reply_header = self.hidden = false
436
+ @lines = []
437
+ @current_block = []
438
+ @content = nil
439
+ end
440
+
441
+ alias quoted? quoted
442
+ alias signature? signature
443
+ alias reply_header? reply_header
444
+ alias hidden? hidden
445
+
446
+ def add_line(line)
447
+ return unless line
448
+ @lines.insert(0, line)
449
+ if line == ""
450
+ @current_block.clear
451
+ else
452
+ @current_block.insert(0, line)
453
+ end
454
+ end
455
+
456
+ def current_block
457
+ @current_block.join("\n")
458
+ end
459
+
460
+ # Builds the string content by joining the lines and reversing them.
461
+ def finish
462
+ @content = @lines.join("\n")
463
+ @lines = @current_block = nil
464
+ end
465
+
466
+ def to_s
467
+ @lines ? @lines.join("\n") : @content
468
+ end
469
+
470
+ def inspect
471
+ "#{super.inspect} : #{to_s.inspect}"
472
+ end
473
+ end
474
+ end
475
+
@@ -0,0 +1,15 @@
1
+ module Replyr
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ def self.source_root
5
+ File.dirname(__FILE__) + "/templates"
6
+ end
7
+
8
+ def copy_files
9
+ copy_file "mailman_server", "script/mailman_server"
10
+ copy_file "mailman_daemon", "script/mailman_daemon"
11
+ template "replyr.rb.erb", "config/initializers/replyr.rb"
12
+ end
13
+ end
14
+ end
15
+ end