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.
- 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
|