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.
- data/Gemfile +30 -12
- data/Gemfile.lock +131 -119
- data/Procfile +1 -1
- data/README.md +1 -1
- data/app/assets/images/notifications.png +0 -0
- data/app/assets/javascripts/application.js.erb +3 -0
- data/app/assets/javascripts/crm_textarea_autocomplete.js +44 -0
- data/app/assets/stylesheets/application.css.erb +2 -0
- data/app/assets/stylesheets/common.scss +7 -11
- data/app/assets/stylesheets/textarea_autocomplete.scss +42 -0
- data/app/controllers/admin/application_controller.rb +5 -5
- data/app/controllers/admin/field_groups_controller.rb +11 -51
- data/app/controllers/admin/fields_controller.rb +13 -59
- data/app/controllers/admin/plugins_controller.rb +1 -4
- data/app/controllers/admin/settings_controller.rb +0 -4
- data/app/controllers/admin/tags_controller.rb +11 -66
- data/app/controllers/admin/users_controller.rb +20 -83
- data/app/controllers/application_controller.rb +83 -69
- data/app/controllers/comments_controller.rb +12 -29
- data/app/controllers/emails_controller.rb +1 -5
- data/app/controllers/entities/accounts_controller.rb +13 -32
- data/app/controllers/entities/campaigns_controller.rb +17 -32
- data/app/controllers/entities/contacts_controller.rb +20 -38
- data/app/controllers/entities/leads_controller.rb +33 -55
- data/app/controllers/entities/opportunities_controller.rb +26 -42
- data/app/controllers/entities_controller.rb +92 -83
- data/app/controllers/home_controller.rb +1 -10
- data/app/controllers/lists_controller.rb +1 -4
- data/app/controllers/{entities/tasks_controller.rb → tasks_controller.rb} +21 -32
- data/app/controllers/users_controller.rb +6 -5
- data/app/helpers/accounts_helper.rb +32 -9
- data/app/helpers/application_helper.rb +15 -1
- data/app/helpers/campaigns_helper.rb +1 -1
- data/app/helpers/comments_helper.rb +11 -1
- data/app/helpers/leads_helper.rb +1 -1
- data/app/helpers/opportunities_helper.rb +1 -1
- data/app/{models/mailers/notifier.rb → mailers/dropbox_mailer.rb} +5 -16
- data/app/mailers/subscription_mailer.rb +37 -0
- data/{lib/tasks/dropbox.rake → app/mailers/user_mailer.rb} +11 -13
- data/app/models/entities/account.rb +3 -1
- data/app/models/entities/campaign.rb +3 -1
- data/app/models/entities/contact.rb +3 -1
- data/app/models/entities/lead.rb +6 -5
- data/app/models/entities/opportunity.rb +3 -1
- data/app/models/fields/field.rb +1 -1
- data/app/models/polymorphic/comment.rb +34 -0
- data/app/models/{entities → polymorphic}/task.rb +16 -3
- data/app/models/setting.rb +15 -15
- data/app/models/users/ability.rb +12 -5
- data/app/models/users/user.rb +7 -2
- data/app/views/accounts/index.html.haml +1 -1
- data/app/views/accounts/index.js.rjs +1 -1
- data/app/views/admin/plugins/index.html.haml +1 -7
- data/app/views/{shared/auto_complete.html.haml → application/_auto_complete.html.haml} +0 -0
- data/app/views/{shared → application}/index.atom.builder +1 -1
- data/app/views/{shared → application}/index.rss.builder +1 -1
- data/app/views/campaigns/index.html.haml +1 -1
- data/app/views/campaigns/index.js.rjs +1 -1
- data/app/views/comments/_new.html.haml +6 -0
- data/app/views/comments/_subscription_links.html.haml +13 -0
- data/app/views/comments/new.js.rjs +2 -0
- data/app/views/contacts/_top_section.html.haml +3 -13
- data/app/views/contacts/index.html.haml +1 -1
- data/app/views/contacts/index.js.rjs +1 -1
- data/app/views/{notifier/dropbox_ack_notification.html.haml → dropbox_mailer/dropbox_notification.html.haml} +2 -2
- data/app/views/{shared → entities}/attach.js.rjs +1 -1
- data/app/views/entities/contacts.js.rjs +1 -1
- data/app/views/{shared/discard.rjs → entities/discard.js.rjs} +0 -0
- data/app/views/entities/leads.js.rjs +1 -1
- data/app/views/entities/opportunities.js.rjs +1 -1
- data/app/views/entities/subscription_update.js.rjs +4 -0
- data/app/views/entities/versions.js.rjs +1 -1
- data/app/views/layouts/_footer.html.haml +1 -1
- data/app/views/layouts/application.html.haml +3 -0
- data/app/views/leads/_contact.html.haml +1 -0
- data/app/views/leads/index.html.haml +1 -1
- data/app/views/leads/index.js.rjs +1 -1
- data/app/views/opportunities/_top_section.html.haml +4 -14
- data/app/views/opportunities/index.html.haml +1 -1
- data/app/views/opportunities/index.js.rjs +1 -1
- data/app/views/subscription_mailer/comment_notification.text.erb +7 -0
- data/app/views/{notifier → user_mailer}/password_reset_instructions.html.haml +0 -0
- data/config/application.rb +3 -1
- data/config/environments/development.rb +1 -1
- data/config/environments/test.rb +3 -0
- data/config/initializers/action_mailer.rb +8 -5
- data/config/initializers/cancan.rb +151 -0
- data/config/initializers/constants.rb +1 -0
- data/config/initializers/locale.rb +20 -0
- data/config/initializers/paper_trail.rb +4 -5
- data/config/initializers/relative_url_root.rb +0 -1
- data/config/initializers/squeel.rb +5 -0
- data/config/locales/cz_fat_free_crm.yml +3 -3
- data/config/locales/de.yml +2 -2
- data/config/locales/de_fat_free_crm.yml +651 -596
- data/config/locales/en-GB_fat_free_crm.yml +3 -3
- data/config/locales/en-US_fat_free_crm.yml +13 -3
- data/config/locales/es_fat_free_crm.yml +3 -3
- data/config/locales/fr-CA_fat_free_crm.yml +3 -3
- data/config/locales/fr_fat_free_crm.yml +3 -3
- data/config/locales/it_fat_free_crm.yml +3 -3
- data/config/locales/pl_fat_free_crm.yml +3 -3
- data/config/locales/pt-BR_fat_free_crm.yml +3 -3
- data/config/locales/ru_fat_free_crm.yml +3 -3
- data/config/locales/sv-SE_fat_free_crm.yml +3 -3
- data/config/locales/th_fat_free_crm.yml +3 -3
- data/config/routes.rb +10 -0
- data/config/settings.default.yml +29 -10
- data/config/unicorn.rb +4 -0
- data/db/migrate/20111201030535_add_field_groups_klass_name.rb +3 -1
- data/db/migrate/20120314080441_add_subscribed_users_to_entities.rb +23 -0
- data/db/migrate/20120405080727_change_subscribed_users_to_set.rb +24 -0
- data/db/migrate/20120405080742_change_further_subscribed_users_to_set.rb +27 -0
- data/db/migrate/20120413034923_add_index_on_versions_item_type.rb +5 -0
- data/db/schema.rb +109 -126
- data/fat_free_crm.gemspec +12 -18
- data/lib/fat_free_crm.rb +0 -1
- data/lib/fat_free_crm/core_ext/array.rb +1 -0
- data/lib/fat_free_crm/gem_dependencies.rb +1 -0
- data/lib/fat_free_crm/mail_processor/base.rb +226 -0
- data/lib/fat_free_crm/mail_processor/comment_replies.rb +86 -0
- data/lib/fat_free_crm/mail_processor/dropbox.rb +288 -0
- data/lib/fat_free_crm/permissions.rb +6 -19
- data/lib/fat_free_crm/renderers.rb +0 -8
- data/lib/fat_free_crm/tabs.rb +1 -1
- data/lib/fat_free_crm/version.rb +1 -1
- data/lib/plugins/country_select/lib/country_select.rb +2 -2
- data/lib/tasks/mail_processing.rake +60 -0
- data/spec/controllers/admin/users_controller_spec.rb +0 -2
- data/spec/controllers/{accounts_controller_spec.rb → entities/accounts_controller_spec.rb} +7 -9
- data/spec/controllers/{campaigns_controller_spec.rb → entities/campaigns_controller_spec.rb} +7 -7
- data/spec/controllers/{contacts_controller_spec.rb → entities/contacts_controller_spec.rb} +5 -9
- data/spec/controllers/{leads_controller_spec.rb → entities/leads_controller_spec.rb} +7 -9
- data/spec/controllers/{opportunities_controller_spec.rb → entities/opportunities_controller_spec.rb} +8 -15
- data/spec/controllers/tasks_controller_spec.rb +1 -5
- data/spec/controllers/users_controller_spec.rb +5 -9
- data/spec/factories/subscription_factories.rb +6 -0
- data/spec/lib/mail_processor/base_spec.rb +164 -0
- data/spec/lib/mail_processor/comment_replies_spec.rb +63 -0
- data/spec/lib/{dropbox_spec.rb → mail_processor/dropbox_spec.rb} +73 -181
- data/spec/lib/mail_processor/sample_emails/dropbox.rb +167 -0
- data/spec/mailers/subscription_mailer_spec.rb +17 -0
- data/spec/models/{base → entities}/account_contact_spec.rb +0 -0
- data/spec/models/{base → entities}/account_opportunity_spec.rb +0 -0
- data/spec/models/{base → entities}/account_spec.rb +4 -0
- data/spec/models/{base → entities}/campaign_spec.rb +4 -0
- data/spec/models/{base → entities}/contact_opportunity_spec.rb +0 -0
- data/spec/models/{base → entities}/contact_spec.rb +4 -0
- data/spec/models/{base → entities}/lead_spec.rb +4 -0
- data/spec/models/{base → entities}/opportunity_spec.rb +4 -0
- data/spec/models/polymorphic/comment_spec.rb +15 -0
- data/spec/models/{base → polymorphic}/task_spec.rb +124 -30
- data/spec/models/polymorphic/version_spec.rb +1 -1
- data/spec/shared/controllers.rb +5 -7
- data/spec/shared/models.rb +46 -0
- data/spec/spec_helper.rb +3 -4
- data/spec/support/mail_processor_mocks.rb +30 -0
- data/spec/support/uploaded_file.rb +3 -0
- data/spec/views/{common → application}/auto_complete.haml_spec.rb +1 -1
- data/vendor/assets/images/jquery-ui/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-bg_flat_10_000000_40x100.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-icons_222222_256x240.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-icons_228ef1_256x240.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-icons_ef8c08_256x240.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-icons_ffd27a_256x240.png +0 -0
- data/vendor/assets/images/jquery-ui/ui-icons_ffffff_256x240.png +0 -0
- data/vendor/assets/javascripts/textarea_autocomplete.js +605 -0
- data/vendor/assets/stylesheets/jquery-ui.custom.css.erb +565 -0
- metadata +234 -154
- data/config/locales/simple_form.en.yml +0 -24
- data/lib/fat_free_crm/dropbox.rb +0 -439
- 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.
|
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
|
-
|
38
|
-
|
39
|
-
gem.
|
40
|
-
gem.
|
41
|
-
gem.
|
42
|
-
gem.
|
43
|
-
gem.
|
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
@@ -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
|
@@ -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
|