fat_free_crm 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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