fat_free_crm 0.11.1 → 0.11.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fat_free_crm might be problematic. Click here for more details.

Files changed (179) hide show
  1. data/Gemfile +30 -12
  2. data/Gemfile.lock +131 -119
  3. data/Procfile +1 -1
  4. data/README.md +1 -1
  5. data/app/assets/images/notifications.png +0 -0
  6. data/app/assets/javascripts/application.js.erb +3 -0
  7. data/app/assets/javascripts/crm_textarea_autocomplete.js +44 -0
  8. data/app/assets/stylesheets/application.css.erb +2 -0
  9. data/app/assets/stylesheets/common.scss +7 -11
  10. data/app/assets/stylesheets/textarea_autocomplete.scss +42 -0
  11. data/app/controllers/admin/application_controller.rb +5 -5
  12. data/app/controllers/admin/field_groups_controller.rb +11 -51
  13. data/app/controllers/admin/fields_controller.rb +13 -59
  14. data/app/controllers/admin/plugins_controller.rb +1 -4
  15. data/app/controllers/admin/settings_controller.rb +0 -4
  16. data/app/controllers/admin/tags_controller.rb +11 -66
  17. data/app/controllers/admin/users_controller.rb +20 -83
  18. data/app/controllers/application_controller.rb +83 -69
  19. data/app/controllers/comments_controller.rb +12 -29
  20. data/app/controllers/emails_controller.rb +1 -5
  21. data/app/controllers/entities/accounts_controller.rb +13 -32
  22. data/app/controllers/entities/campaigns_controller.rb +17 -32
  23. data/app/controllers/entities/contacts_controller.rb +20 -38
  24. data/app/controllers/entities/leads_controller.rb +33 -55
  25. data/app/controllers/entities/opportunities_controller.rb +26 -42
  26. data/app/controllers/entities_controller.rb +92 -83
  27. data/app/controllers/home_controller.rb +1 -10
  28. data/app/controllers/lists_controller.rb +1 -4
  29. data/app/controllers/{entities/tasks_controller.rb → tasks_controller.rb} +21 -32
  30. data/app/controllers/users_controller.rb +6 -5
  31. data/app/helpers/accounts_helper.rb +32 -9
  32. data/app/helpers/application_helper.rb +15 -1
  33. data/app/helpers/campaigns_helper.rb +1 -1
  34. data/app/helpers/comments_helper.rb +11 -1
  35. data/app/helpers/leads_helper.rb +1 -1
  36. data/app/helpers/opportunities_helper.rb +1 -1
  37. data/app/{models/mailers/notifier.rb → mailers/dropbox_mailer.rb} +5 -16
  38. data/app/mailers/subscription_mailer.rb +37 -0
  39. data/{lib/tasks/dropbox.rake → app/mailers/user_mailer.rb} +11 -13
  40. data/app/models/entities/account.rb +3 -1
  41. data/app/models/entities/campaign.rb +3 -1
  42. data/app/models/entities/contact.rb +3 -1
  43. data/app/models/entities/lead.rb +6 -5
  44. data/app/models/entities/opportunity.rb +3 -1
  45. data/app/models/fields/field.rb +1 -1
  46. data/app/models/polymorphic/comment.rb +34 -0
  47. data/app/models/{entities → polymorphic}/task.rb +16 -3
  48. data/app/models/setting.rb +15 -15
  49. data/app/models/users/ability.rb +12 -5
  50. data/app/models/users/user.rb +7 -2
  51. data/app/views/accounts/index.html.haml +1 -1
  52. data/app/views/accounts/index.js.rjs +1 -1
  53. data/app/views/admin/plugins/index.html.haml +1 -7
  54. data/app/views/{shared/auto_complete.html.haml → application/_auto_complete.html.haml} +0 -0
  55. data/app/views/{shared → application}/index.atom.builder +1 -1
  56. data/app/views/{shared → application}/index.rss.builder +1 -1
  57. data/app/views/campaigns/index.html.haml +1 -1
  58. data/app/views/campaigns/index.js.rjs +1 -1
  59. data/app/views/comments/_new.html.haml +6 -0
  60. data/app/views/comments/_subscription_links.html.haml +13 -0
  61. data/app/views/comments/new.js.rjs +2 -0
  62. data/app/views/contacts/_top_section.html.haml +3 -13
  63. data/app/views/contacts/index.html.haml +1 -1
  64. data/app/views/contacts/index.js.rjs +1 -1
  65. data/app/views/{notifier/dropbox_ack_notification.html.haml → dropbox_mailer/dropbox_notification.html.haml} +2 -2
  66. data/app/views/{shared → entities}/attach.js.rjs +1 -1
  67. data/app/views/entities/contacts.js.rjs +1 -1
  68. data/app/views/{shared/discard.rjs → entities/discard.js.rjs} +0 -0
  69. data/app/views/entities/leads.js.rjs +1 -1
  70. data/app/views/entities/opportunities.js.rjs +1 -1
  71. data/app/views/entities/subscription_update.js.rjs +4 -0
  72. data/app/views/entities/versions.js.rjs +1 -1
  73. data/app/views/layouts/_footer.html.haml +1 -1
  74. data/app/views/layouts/application.html.haml +3 -0
  75. data/app/views/leads/_contact.html.haml +1 -0
  76. data/app/views/leads/index.html.haml +1 -1
  77. data/app/views/leads/index.js.rjs +1 -1
  78. data/app/views/opportunities/_top_section.html.haml +4 -14
  79. data/app/views/opportunities/index.html.haml +1 -1
  80. data/app/views/opportunities/index.js.rjs +1 -1
  81. data/app/views/subscription_mailer/comment_notification.text.erb +7 -0
  82. data/app/views/{notifier → user_mailer}/password_reset_instructions.html.haml +0 -0
  83. data/config/application.rb +3 -1
  84. data/config/environments/development.rb +1 -1
  85. data/config/environments/test.rb +3 -0
  86. data/config/initializers/action_mailer.rb +8 -5
  87. data/config/initializers/cancan.rb +151 -0
  88. data/config/initializers/constants.rb +1 -0
  89. data/config/initializers/locale.rb +20 -0
  90. data/config/initializers/paper_trail.rb +4 -5
  91. data/config/initializers/relative_url_root.rb +0 -1
  92. data/config/initializers/squeel.rb +5 -0
  93. data/config/locales/cz_fat_free_crm.yml +3 -3
  94. data/config/locales/de.yml +2 -2
  95. data/config/locales/de_fat_free_crm.yml +651 -596
  96. data/config/locales/en-GB_fat_free_crm.yml +3 -3
  97. data/config/locales/en-US_fat_free_crm.yml +13 -3
  98. data/config/locales/es_fat_free_crm.yml +3 -3
  99. data/config/locales/fr-CA_fat_free_crm.yml +3 -3
  100. data/config/locales/fr_fat_free_crm.yml +3 -3
  101. data/config/locales/it_fat_free_crm.yml +3 -3
  102. data/config/locales/pl_fat_free_crm.yml +3 -3
  103. data/config/locales/pt-BR_fat_free_crm.yml +3 -3
  104. data/config/locales/ru_fat_free_crm.yml +3 -3
  105. data/config/locales/sv-SE_fat_free_crm.yml +3 -3
  106. data/config/locales/th_fat_free_crm.yml +3 -3
  107. data/config/routes.rb +10 -0
  108. data/config/settings.default.yml +29 -10
  109. data/config/unicorn.rb +4 -0
  110. data/db/migrate/20111201030535_add_field_groups_klass_name.rb +3 -1
  111. data/db/migrate/20120314080441_add_subscribed_users_to_entities.rb +23 -0
  112. data/db/migrate/20120405080727_change_subscribed_users_to_set.rb +24 -0
  113. data/db/migrate/20120405080742_change_further_subscribed_users_to_set.rb +27 -0
  114. data/db/migrate/20120413034923_add_index_on_versions_item_type.rb +5 -0
  115. data/db/schema.rb +109 -126
  116. data/fat_free_crm.gemspec +12 -18
  117. data/lib/fat_free_crm.rb +0 -1
  118. data/lib/fat_free_crm/core_ext/array.rb +1 -0
  119. data/lib/fat_free_crm/gem_dependencies.rb +1 -0
  120. data/lib/fat_free_crm/mail_processor/base.rb +226 -0
  121. data/lib/fat_free_crm/mail_processor/comment_replies.rb +86 -0
  122. data/lib/fat_free_crm/mail_processor/dropbox.rb +288 -0
  123. data/lib/fat_free_crm/permissions.rb +6 -19
  124. data/lib/fat_free_crm/renderers.rb +0 -8
  125. data/lib/fat_free_crm/tabs.rb +1 -1
  126. data/lib/fat_free_crm/version.rb +1 -1
  127. data/lib/plugins/country_select/lib/country_select.rb +2 -2
  128. data/lib/tasks/mail_processing.rake +60 -0
  129. data/spec/controllers/admin/users_controller_spec.rb +0 -2
  130. data/spec/controllers/{accounts_controller_spec.rb → entities/accounts_controller_spec.rb} +7 -9
  131. data/spec/controllers/{campaigns_controller_spec.rb → entities/campaigns_controller_spec.rb} +7 -7
  132. data/spec/controllers/{contacts_controller_spec.rb → entities/contacts_controller_spec.rb} +5 -9
  133. data/spec/controllers/{leads_controller_spec.rb → entities/leads_controller_spec.rb} +7 -9
  134. data/spec/controllers/{opportunities_controller_spec.rb → entities/opportunities_controller_spec.rb} +8 -15
  135. data/spec/controllers/tasks_controller_spec.rb +1 -5
  136. data/spec/controllers/users_controller_spec.rb +5 -9
  137. data/spec/factories/subscription_factories.rb +6 -0
  138. data/spec/lib/mail_processor/base_spec.rb +164 -0
  139. data/spec/lib/mail_processor/comment_replies_spec.rb +63 -0
  140. data/spec/lib/{dropbox_spec.rb → mail_processor/dropbox_spec.rb} +73 -181
  141. data/spec/lib/mail_processor/sample_emails/dropbox.rb +167 -0
  142. data/spec/mailers/subscription_mailer_spec.rb +17 -0
  143. data/spec/models/{base → entities}/account_contact_spec.rb +0 -0
  144. data/spec/models/{base → entities}/account_opportunity_spec.rb +0 -0
  145. data/spec/models/{base → entities}/account_spec.rb +4 -0
  146. data/spec/models/{base → entities}/campaign_spec.rb +4 -0
  147. data/spec/models/{base → entities}/contact_opportunity_spec.rb +0 -0
  148. data/spec/models/{base → entities}/contact_spec.rb +4 -0
  149. data/spec/models/{base → entities}/lead_spec.rb +4 -0
  150. data/spec/models/{base → entities}/opportunity_spec.rb +4 -0
  151. data/spec/models/polymorphic/comment_spec.rb +15 -0
  152. data/spec/models/{base → polymorphic}/task_spec.rb +124 -30
  153. data/spec/models/polymorphic/version_spec.rb +1 -1
  154. data/spec/shared/controllers.rb +5 -7
  155. data/spec/shared/models.rb +46 -0
  156. data/spec/spec_helper.rb +3 -4
  157. data/spec/support/mail_processor_mocks.rb +30 -0
  158. data/spec/support/uploaded_file.rb +3 -0
  159. data/spec/views/{common → application}/auto_complete.haml_spec.rb +1 -1
  160. data/vendor/assets/images/jquery-ui/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  161. data/vendor/assets/images/jquery-ui/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  162. data/vendor/assets/images/jquery-ui/ui-bg_flat_10_000000_40x100.png +0 -0
  163. data/vendor/assets/images/jquery-ui/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  164. data/vendor/assets/images/jquery-ui/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  165. data/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png +0 -0
  166. data/vendor/assets/images/jquery-ui/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  167. data/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  168. data/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  169. data/vendor/assets/images/jquery-ui/ui-icons_222222_256x240.png +0 -0
  170. data/vendor/assets/images/jquery-ui/ui-icons_228ef1_256x240.png +0 -0
  171. data/vendor/assets/images/jquery-ui/ui-icons_ef8c08_256x240.png +0 -0
  172. data/vendor/assets/images/jquery-ui/ui-icons_ffd27a_256x240.png +0 -0
  173. data/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png +0 -0
  174. data/vendor/assets/javascripts/textarea_autocomplete.js +605 -0
  175. data/vendor/assets/stylesheets/jquery-ui.custom.css.erb +565 -0
  176. metadata +234 -154
  177. data/config/locales/simple_form.en.yml +0 -24
  178. data/lib/fat_free_crm/dropbox.rb +0 -439
  179. data/spec/lib/dropbox/email_samples.rb +0 -77
data/fat_free_crm.gemspec CHANGED
@@ -22,28 +22,22 @@ Gem::Specification.new do |gem|
22
22
  gem.add_dependency 'authlogic', '~> 3.1.0'
23
23
  gem.add_dependency 'acts_as_commentable', '~> 3.0.1'
24
24
  gem.add_dependency 'acts-as-taggable-on', '~> 2.2.1'
25
- gem.add_dependency 'responds_to_parent'
26
25
  gem.add_dependency 'dynamic_form'
27
26
  gem.add_dependency 'haml', '~> 3.1.3'
28
- gem.add_dependency 'sass', '~> 3.1.15'
27
+ gem.add_dependency 'sass', '~> 3.1.10'
29
28
  gem.add_dependency 'acts_as_list', '~> 0.1.4'
30
29
  gem.add_dependency 'ffaker', '>= 1.12.0'
31
- gem.add_dependency 'uglifier'
32
- gem.add_dependency 'chosen-rails'
33
- gem.add_dependency 'ajax-chosen-rails', '>= 0.1.5'
34
- gem.add_dependency 'ransack'
35
30
  gem.add_dependency 'cancan'
31
+ gem.add_dependency 'premailer'
32
+ gem.add_dependency 'nokogiri'
33
+ gem.add_dependency 'squeel', '~> 0.9.3'
36
34
 
37
- gem.add_development_dependency 'rspec-rails', '~> 2.8.1'
38
- gem.add_development_dependency 'capybara'
39
- gem.add_development_dependency 'sass-rails'
40
- gem.add_development_dependency 'coffee-rails'
41
- gem.add_development_dependency 'therubyracer'
42
- gem.add_development_dependency 'spork'
43
- gem.add_development_dependency 'database_cleaner'
44
- gem.add_development_dependency 'fuubar'
45
- gem.add_development_dependency 'factory_girl', '~> 2.6.1'
46
- gem.add_development_dependency 'factory_girl_rails', '~> 1.7.0'
47
- end
48
-
35
+ # FatFreeCRM has released it's own versions of the following gems:
36
+ #-----------------------------------------------------------------
37
+ gem.add_dependency 'ransack_ffcrm', '~> 0.6.0'
38
+ gem.add_dependency 'chosen-rails_ffcrm'
39
+ gem.add_dependency 'ajax-chosen-rails', '>= 0.2.0' # (now depends on chosen-rails_ffcrm)
40
+ gem.add_dependency 'responds_to_parent_ffcrm'
41
+ gem.add_dependency 'email_reply_parser_ffcrm'
49
42
 
43
+ end
data/lib/fat_free_crm.rb CHANGED
@@ -54,5 +54,4 @@ require "fat_free_crm/fields"
54
54
  require "fat_free_crm/sortable"
55
55
  require "fat_free_crm/tabs"
56
56
  require "fat_free_crm/callback"
57
- require "fat_free_crm/dropbox" if defined?(::Rake)
58
57
  require "fat_free_crm/plugin"
@@ -41,6 +41,7 @@ class Array
41
41
  else
42
42
  item.send(column)
43
43
  end
44
+ value = value.to_a.join(',') if [Set, Array].include?(value.class)
44
45
  value.to_s.wrap(%Q|<Cell><Data ss:Type="#{value.respond_to?(:abs) ? 'Number' : 'String'}">|, '</Data></Cell>')
45
46
  end.join.wrap('<Row>', '</Row>')
46
47
  end
@@ -34,6 +34,7 @@ require 'ajax-chosen-rails'
34
34
  require 'ransack'
35
35
  require 'paper_trail'
36
36
  require 'cancan'
37
+ require 'squeel'
37
38
 
38
39
  # Load redcloth if available (for textile markup in emails)
39
40
  begin
@@ -0,0 +1,226 @@
1
+ # Fat Free CRM
2
+ # Copyright (C) 2008-2011 by Michael Dvorkin
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ #------------------------------------------------------------------------------
17
+
18
+ require 'net/imap'
19
+ require 'mail'
20
+ require 'email_reply_parser'
21
+ require 'premailer'
22
+ require 'nokogiri'
23
+
24
+ module FatFreeCRM
25
+ module MailProcessor
26
+ class Base
27
+ KEYWORDS = %w(account campaign contact lead opportunity).freeze
28
+
29
+ #--------------------------------------------------------------------------------------
30
+ def initialize
31
+ @archived, @discarded, @dry_run = 0, 0, false
32
+ end
33
+
34
+ # Setup imap folders in settings.
35
+ #--------------------------------------------------------------------------------------
36
+ def setup
37
+ log "connecting to #{@settings[:server]}..."
38
+ connect!(:setup => true) or return nil
39
+ log "logged in to #{@settings[:server]}, checking folders..."
40
+ folders = [ @settings[:scan_folder] ]
41
+ folders << @settings[:move_to_folder] unless @settings[:move_to_folder].blank?
42
+ folders << @settings[:move_invalid_to_folder] unless @settings[:move_invalid_to_folder].blank?
43
+
44
+ # Open (or create) destination folder in read-write mode.
45
+ folders.each do |folder|
46
+ if @imap.list("", folder)
47
+ log "folder #{folder} OK"
48
+ else
49
+ log "folder #{folder} missing, creating..."
50
+ @imap.create(folder)
51
+ end
52
+ end
53
+ rescue => e
54
+ $stderr.puts "setup error #{e.inspect}"
55
+ ensure
56
+ disconnect!
57
+ end
58
+
59
+ #--------------------------------------------------------------------------------------
60
+ def run(dry_run = false)
61
+ if @dry_run = dry_run
62
+ log "Not discarding or archiving any new messages..."
63
+ end
64
+ connect! or return nil
65
+ with_new_emails do |uid, email|
66
+ # Subclasses must define a #process method that takes arguments: uid, email
67
+ process(uid, email)
68
+ archive(uid)
69
+ end
70
+ ensure
71
+ log "messages processed=#{@archived + @discarded} archived=#{@archived} discarded=#{@discarded}"
72
+ disconnect!
73
+ end
74
+
75
+ private
76
+
77
+ # Connects to the imap server with the loaded settings
78
+ #------------------------------------------------------------------------------
79
+ def connect!(options = {})
80
+ log "connecting & logging in to #{@settings[:server]}..."
81
+ @imap = Net::IMAP.new(@settings[:server], @settings[:port], @settings[:ssl])
82
+ @imap.login(@settings[:user], @settings[:password])
83
+ log "logged in to #{@settings[:server]}, checking folders..."
84
+ @imap.select(@settings[:scan_folder]) unless options[:setup]
85
+ @imap
86
+ rescue Exception => e
87
+ $stderr.puts "Could not login to the IMAP server: #{e.inspect}" unless Rails.env == "test"
88
+ nil
89
+ end
90
+
91
+ #------------------------------------------------------------------------------
92
+ def disconnect!
93
+ if @imap
94
+ @imap.logout
95
+ unless @imap.disconnected?
96
+ @imap.disconnect rescue nil
97
+ end
98
+ end
99
+ end
100
+
101
+ #--------------------------------------------------------------------------------------
102
+ def with_new_emails
103
+ @imap.uid_search(['NOT', 'SEEN']).each do |uid|
104
+ begin
105
+ email = Mail.new(@imap.uid_fetch(uid, 'RFC822').first.attr['RFC822'])
106
+ log "fetched new message...", email
107
+ if is_valid?(email) && sent_from_known_user?(email)
108
+ yield(uid, email)
109
+ else
110
+ discard(uid)
111
+ end
112
+ rescue Exception => e
113
+ if ["test", "development"].include?(Rails.env)
114
+ $stderr.puts e
115
+ $stderr.puts e.backtrace
116
+ end
117
+ log "error processing email: #{e.inspect}", email
118
+ discard(uid)
119
+ end
120
+
121
+ if @dry_run
122
+ log "Marking message as unread"
123
+ @imap.uid_store(uid, "-FLAGS", [:Seen])
124
+ end
125
+ end
126
+ end
127
+
128
+
129
+ # Discard message (not valid) action based on settings
130
+ #------------------------------------------------------------------------------
131
+ def discard(uid)
132
+ if @dry_run
133
+ log "Not discarding message"
134
+ else
135
+ if @settings[:move_invalid_to_folder]
136
+ @imap.uid_copy(uid, @settings[:move_invalid_to_folder])
137
+ end
138
+ @imap.uid_store(uid, "+FLAGS", [:Deleted])
139
+ end
140
+ @discarded += 1
141
+ end
142
+
143
+ # Archive message (valid) action based on settings
144
+ #------------------------------------------------------------------------------
145
+ def archive(uid)
146
+ if @dry_run
147
+ log "Not archiving message"
148
+ else
149
+ if @settings[:move_to_folder]
150
+ @imap.uid_copy(uid, @settings[:move_to_folder])
151
+ end
152
+ @imap.uid_store(uid, "+FLAGS", [:Seen])
153
+ end
154
+ @archived += 1
155
+ end
156
+
157
+ #------------------------------------------------------------------------------
158
+ def is_valid?(email)
159
+ valid = email.content_type != "text/html"
160
+ log("not a text message, discarding") unless valid
161
+ valid
162
+ end
163
+
164
+ #------------------------------------------------------------------------------
165
+ def sent_from_known_user?(email)
166
+ email_address = email.from.first
167
+ known = !find_sender(email_address).nil?
168
+ log("sent by unknown user #{email_address}, discarding") unless known
169
+ known
170
+ end
171
+
172
+ #------------------------------------------------------------------------------
173
+ def find_sender(email_address)
174
+ if @sender = User.first(:conditions => [ "(lower(email) = ? OR lower(alt_email) = ?) AND suspended_at IS NULL", email_address.downcase, email_address.downcase ])
175
+ # Set the PaperTrail user for versions (if user is found)
176
+ PaperTrail.whodunnit = @sender.id.to_s
177
+ end
178
+ end
179
+
180
+ #--------------------------------------------------------------------------------------
181
+ def sender_has_permissions_for?(asset)
182
+ return true if asset.access == "Public"
183
+ return true if asset.user_id == @sender.id || asset.assigned_to == @sender.id
184
+ return true if asset.access == "Shared" && Permission.where('user_id = ? AND asset_id = ? AND asset_type = ?', @sender.id, asset.id, asset.class.to_s).count > 0
185
+
186
+ false
187
+ end
188
+
189
+ # Centralized logging.
190
+ #--------------------------------------------------------------------------------------
191
+ def log(message, email = nil)
192
+ unless %w(test cucumber).include?(Rails.env)
193
+ klass = self.class.to_s.split("::").last
194
+ klass << " [Dry Run]" if @dry_run
195
+ puts "[#{Time.now.rfc822}] #{klass}: #{message}"
196
+ puts "[#{Time.now.rfc822}] #{klass}: From: #{email.from}, Subject: #{email.subject} (#{email.message_id})" if email
197
+ end
198
+ end
199
+
200
+ # Returns the plain-text version of an email, or strips html tags
201
+ # if only html is present.
202
+ #--------------------------------------------------------------------------------------
203
+ def plain_text_body(email)
204
+
205
+ # Extract all parts including nested
206
+ parts = if email.multipart?
207
+ email.parts.map {|p| p.multipart? ? p.parts : p}.flatten
208
+ else
209
+ [email]
210
+ end
211
+
212
+ if text_part = parts.detect {|p| p.content_type.include?('text/plain')}
213
+ text_body = text_part.body.to_s
214
+
215
+ else
216
+ html_part = parts.detect {|p| p.content_type.include?('text/html')} || email
217
+ text_body = Premailer.new(html_part.body.to_s, :with_html_string => true).to_plain_text
218
+ end
219
+
220
+ # Standardize newline
221
+ text_body.strip.gsub "\r\n", "\n"
222
+ end
223
+
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,86 @@
1
+ # Fat Free CRM
2
+ # Copyright (C) 2008-2011 by Michael Dvorkin
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ #------------------------------------------------------------------------------
17
+
18
+ require 'fat_free_crm/mail_processor/base'
19
+
20
+ module FatFreeCRM
21
+ module MailProcessor
22
+ class CommentReplies < Base
23
+
24
+ # Subject line of email can contain full entity, or shortcuts
25
+ # e.g. [contact:1234] OR [co:1234]
26
+ ENTITY_SHORTCUTS = {
27
+ 'ac' => 'account',
28
+ 'ca' => 'campaign',
29
+ 'co' => 'contact',
30
+ 'le' => 'lead',
31
+ 'op' => 'opportunity',
32
+ 'ta' => 'task'
33
+ }
34
+
35
+ #--------------------------------------------------------------------------------------
36
+ def initialize
37
+ @settings = Setting.email_comment_replies.dup
38
+ super
39
+ end
40
+
41
+ private
42
+
43
+ # Email processing pipeline
44
+ #--------------------------------------------------------------------------------------
45
+ def process(uid, email)
46
+ with_subject_line(email) do |entity_name, entity_id|
47
+ create_comment email, entity_name, entity_id
48
+ end
49
+ end
50
+
51
+
52
+ # Checks the email to detect [entity:id] in the subject.
53
+ #--------------------------------------------------------------------------------------
54
+ def with_subject_line(email)
55
+ if /\[([^:]*):([^\]]*)\]/ =~ email.subject
56
+ entity_name, entity_id = $1, $2
57
+ # Check that entity is a known model
58
+ if ENTITY_SHORTCUTS.values.include?(entity_name)
59
+ yield entity_name, entity_id
60
+ # Check if entity is a 2 letter 'shortcut'
61
+ elsif expanded_entity = ENTITY_SHORTCUTS[entity_name]
62
+ yield expanded_entity, entity_id
63
+ end
64
+ end
65
+ end
66
+
67
+
68
+ # Creates a new comment on an entity
69
+ #--------------------------------------------------------------------------------------
70
+ def create_comment(email, entity_name, entity_id)
71
+ # Find entity from subject params
72
+ if (entity = entity_name.capitalize.constantize.find_by_id(entity_id))
73
+ # Create comment if sender has permissions for entity
74
+ if sender_has_permissions_for?(entity)
75
+ parsed_reply = EmailReplyParser.parse_reply(plain_text_body(email))
76
+ Comment.create :user => @sender,
77
+ :commentable => entity,
78
+ :comment => parsed_reply
79
+ end
80
+ end
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,288 @@
1
+ # Fat Free CRM
2
+ # Copyright (C) 2008-2011 by Michael Dvorkin
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ #------------------------------------------------------------------------------
17
+
18
+ require 'fat_free_crm/mail_processor/base'
19
+
20
+ module FatFreeCRM
21
+ module MailProcessor
22
+ class Dropbox < Base
23
+ KEYWORDS = %w(account campaign contact lead opportunity).freeze
24
+
25
+ #--------------------------------------------------------------------------------------
26
+ def initialize
27
+ # Models are autoloaded, so the following @@assets class variable should only be set
28
+ # when Dropbox is initialized. This needs to be done so that Rake tasks such as
29
+ # 'assets:precompile' can run on Heroku without depending on a database.
30
+ # See: http://devcenter.heroku.com/articles/rails31_heroku_cedar#troubleshooting
31
+ @@assets = [ Account, Contact, Lead ].freeze
32
+ @settings = Setting.email_dropbox.dup
33
+ super
34
+ end
35
+
36
+ private
37
+
38
+ # Email processing pipeline: each steps gets executed if previous one returns false.
39
+ #--------------------------------------------------------------------------------------
40
+ def process(uid, email)
41
+ with_explicit_keyword(email) do |keyword, name|
42
+ data = {"Type" => keyword, "Name" => name}
43
+ find_or_create_and_attach(email, data)
44
+ end and return
45
+
46
+ with_recipients(email) do |recipient|
47
+ find_and_attach(email, recipient)
48
+ end and return
49
+
50
+ with_forwarded_recipient(email) do |recipient|
51
+ find_and_attach(email, recipient)
52
+ end and return
53
+
54
+ with_recipients(email) do |recipient|
55
+ create_and_attach(email, recipient)
56
+ end and return
57
+
58
+ with_forwarded_recipient(email) do |recipient|
59
+ create_and_attach(email, recipient)
60
+ end
61
+ end
62
+
63
+
64
+ # Checks the email to detect keyword on the first line.
65
+ #--------------------------------------------------------------------------------------
66
+ def with_explicit_keyword(email)
67
+ first_line = plain_text_body(email).split("\n").first
68
+ if first_line =~ %r|(#{KEYWORDS.join('|')})[^a-zA-Z0-9]+(.+)$|i
69
+ yield $1.capitalize, $2.strip
70
+ end
71
+ end
72
+
73
+ # Checks the email to detect assets on to/bcc addresses
74
+ #--------------------------------------------------------------------------------------
75
+ def with_recipients(email, options = {})
76
+ recipients = []
77
+ recipients += email.to_addrs unless email.to.blank?
78
+ recipients += email.cc_addrs unless email.cc.blank?
79
+
80
+ # Ignore the dropbox email address, and any address aliases
81
+ ignored_addresses = [ @settings[:address] ]
82
+ if @settings[:address_aliases].is_a?(Array)
83
+ ignored_addresses += @settings[:address_aliases]
84
+ end
85
+ recipients -= ignored_addresses
86
+
87
+ # Process each recipient until email has been attached
88
+ recipients.each do |recipient|
89
+ return true if yield recipient
90
+ end
91
+ false
92
+ end
93
+
94
+ # Checks the email to detect valid email address in body (first email), detect forwarded emails
95
+ #----------------------------------------------------------------------------------------
96
+ def with_forwarded_recipient(email, options = {})
97
+ if plain_text_body(email) =~ /\b([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4})\b/
98
+ yield $1
99
+ end
100
+ end
101
+
102
+ # Process pipe_separated_data or explicit keyword.
103
+ #--------------------------------------------------------------------------------------
104
+ def find_or_create_and_attach(email, data)
105
+ klass = data["Type"].constantize
106
+
107
+ if data["Email"] && klass.new.respond_to?(:email)
108
+ conditions = [
109
+ "(lower(email) = ? OR lower(alt_email) = ?)",
110
+ data["Email"].downcase,
111
+ data["Email"].downcase
112
+ ]
113
+ elsif klass.new.respond_to?(:first_name)
114
+ first_name, *last_name = data["Name"].split
115
+ conditions = if last_name.empty? # Treat single name as last name.
116
+ [ 'last_name LIKE ?', "%#{first_name}" ]
117
+ else
118
+ [ 'first_name LIKE ? AND last_name LIKE ?', "%#{first_name}", "%#{last_name.join(' ')}" ]
119
+ end
120
+ else
121
+ conditions = ['name LIKE ?', "%#{data["Name"]}%"]
122
+ end
123
+
124
+ # Find the asset from deduced conditions
125
+ if asset = klass.where(conditions).first
126
+ if sender_has_permissions_for?(asset)
127
+ attach(email, asset, :strip_first_line)
128
+ else
129
+ log "Sender does not have permissions to attach email to #{data["Type"]} #{data["Email"]} <#{data["Name"]}>"
130
+ end
131
+ else
132
+ log "#{data["Type"]} #{data["Email"]} <#{data["Name"]}> not found, creating new one..."
133
+ asset = klass.create!(default_values(klass, data))
134
+ attach(email, asset, :strip_first_line)
135
+ end
136
+ true
137
+ end
138
+
139
+ #----------------------------------------------------------------------------------------
140
+ def find_and_attach(email, recipient)
141
+ attached = false
142
+ @@assets.each do |klass|
143
+ asset = klass.where(["(lower(email) = ?)", recipient.downcase]).first
144
+
145
+ # Leads and Contacts have an alt_email: try it if lookup by primary email has failed.
146
+ if !asset && klass.column_names.include?("alt_email")
147
+ asset = klass.where(["(lower(alt_email) = ?)", recipient.downcase]).first
148
+ end
149
+
150
+ if asset && sender_has_permissions_for?(asset)
151
+ attach(email, asset)
152
+ attached = true
153
+ end
154
+ end
155
+ attached
156
+ end
157
+
158
+ #----------------------------------------------------------------------------------------
159
+ def create_and_attach(email, recipient)
160
+ contact = Contact.create!(default_values_for_contact(email, recipient))
161
+ attach(email, contact)
162
+ end
163
+
164
+ #----------------------------------------------------------------------------------------
165
+ def attach(email, asset, strip_first_line=false)
166
+ # If 'sent_to' email cannot be found, default to Dropbox email address
167
+ to = email.to.blank? ? @settings[:address] : email.to.join(", ")
168
+ cc = email.cc.blank? ? nil : email.cc.join(", ")
169
+
170
+ email_body = if strip_first_line
171
+ plain_text_body(email).split("\n")[1..-1].join("\n").strip
172
+ else
173
+ plain_text_body(email)
174
+ end
175
+
176
+ Email.create(
177
+ :imap_message_id => email.message_id,
178
+ :user => @sender,
179
+ :mediator => asset,
180
+ :sent_from => email.from.first,
181
+ :sent_to => to,
182
+ :cc => cc,
183
+ :subject => email.subject,
184
+ :body => email_body,
185
+ :received_at => email.date,
186
+ :sent_at => email.date
187
+ )
188
+ asset.touch
189
+
190
+ if asset.is_a?(Lead) && asset.status == "new"
191
+ asset.update_attribute(:status, "contacted")
192
+ end
193
+
194
+ if @settings[:attach_to_account] && asset.respond_to?(:account) && asset.account
195
+ Email.create(
196
+ :imap_message_id => email.message_id,
197
+ :user => @sender,
198
+ :mediator => asset.account,
199
+ :sent_from => email.from.first,
200
+ :sent_to => to,
201
+ :cc => cc,
202
+ :subject => email.subject,
203
+ :body => email_body,
204
+ :received_at => email.date,
205
+ :sent_at => email.date
206
+ )
207
+ asset.account.touch
208
+ end
209
+ end
210
+
211
+ #----------------------------------------------------------------------------------------
212
+ def default_values(klass, data)
213
+ data = data.dup
214
+ keyword = data.delete("Type").capitalize
215
+
216
+ defaults = {
217
+ :user => @sender,
218
+ :access => default_access
219
+ }
220
+
221
+ case keyword
222
+ when "Account", "Campaign", "Opportunity"
223
+ defaults[:status] = "planned" if keyword == "Campaign" # TODO: I18n
224
+ defaults[:stage] = "prospecting" if keyword == "Opportunity" # TODO: I18n
225
+
226
+ when "Contact", "Lead"
227
+ first_name, *last_name = data.delete("Name").split(' ')
228
+ defaults[:first_name] = first_name
229
+ defaults[:last_name] = (last_name.any? ? last_name.join(" ") : "(unknown)")
230
+ defaults[:status] = "contacted" if keyword == "Lead" # TODO: I18n
231
+ end
232
+
233
+ data.each do |key, value|
234
+ key = key.downcase
235
+ defaults[key.to_sym] = value if klass.new.respond_to?(key + '=')
236
+ end
237
+
238
+ defaults
239
+ end
240
+
241
+ #----------------------------------------------------------------------------------------
242
+ def default_values_for_contact(email, recipient)
243
+ recipient_local, recipient_domain = recipient.split('@')
244
+
245
+ defaults = {
246
+ :user => @sender,
247
+ :first_name => recipient_local.capitalize,
248
+ :last_name => "(unknown)",
249
+ :email => recipient,
250
+ :access => default_access
251
+ }
252
+
253
+ # Search for domain name in Accounts.
254
+ account = Account.where('(lower(email) like ? OR lower(website) like ?)', "%#{recipient_domain.downcase}", "%#{recipient_domain.downcase}%").first
255
+ if account
256
+ log "asociating new contact #{recipient} with the account #{account.name}"
257
+ defaults[:account] = account
258
+ else
259
+ log "creating new account #{recipient_domain.capitalize} for the contact #{recipient}"
260
+ defaults[:account] = Account.create!(
261
+ :user => @sender,
262
+ :email => recipient,
263
+ :name => recipient_domain.capitalize,
264
+ :access => default_access
265
+ )
266
+ end
267
+ defaults
268
+ end
269
+
270
+ # If default access is 'Shared' then change it to 'Private' because we don't know how
271
+ # to choose anyone to share it with here.
272
+ #--------------------------------------------------------------------------------------
273
+ def default_access
274
+ Setting.default_access == "Shared" ? 'Private' : Setting.default_access
275
+ end
276
+
277
+
278
+ # Notify users with the results of the operations
279
+ #--------------------------------------------------------------------------------------
280
+ def notify(email, mediator_links)
281
+ DropboxMailer.create_dropbox_notification(
282
+ @sender, @settings[:address], email, mediator_links
283
+ ).deliver
284
+ end
285
+
286
+ end
287
+ end
288
+ end