icfs 0.1.3 → 0.2.0
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.
- checksums.yaml +4 -4
- data/bin/icfs_demo_fcgi.rb +2 -0
- data/{bin/icfs_demo_ssl_gen.rb → devel/demo/ssl_gen.rb} +25 -13
- data/devel/demo/ssl_gen.yml +14 -0
- data/devel/icfs-wrk/Dockerfile +1 -1
- data/devel/run/base.rb +92 -0
- data/devel/run/copy-s3.rb +2 -0
- data/devel/run/email.rb +36 -0
- data/devel/run/email_imap.rb +43 -0
- data/devel/run/email_smime.rb +47 -0
- data/devel/run/init-icfs.rb +2 -0
- data/devel/run/webrick.rb +5 -57
- data/lib/icfs/api.rb +101 -90
- data/lib/icfs/cache.rb +2 -0
- data/lib/icfs/cache_elastic.rb +127 -125
- data/lib/icfs/{web/config.rb → config.rb} +3 -3
- data/lib/icfs/{web/config_redis.rb → config_redis.rb} +8 -8
- data/lib/icfs/{web/config_s3.rb → config_s3.rb} +8 -8
- data/lib/icfs/demo/auth.rb +5 -7
- data/lib/icfs/demo/static.rb +2 -0
- data/lib/icfs/elastic.rb +10 -8
- data/lib/icfs/email/basic.rb +242 -0
- data/lib/icfs/email/core.rb +293 -0
- data/lib/icfs/email/from.rb +52 -0
- data/lib/icfs/email/imap.rb +148 -0
- data/lib/icfs/email/smime.rb +139 -0
- data/lib/icfs/items.rb +5 -3
- data/lib/icfs/store.rb +20 -18
- data/lib/icfs/store_fs.rb +7 -5
- data/lib/icfs/store_s3.rb +4 -2
- data/lib/icfs/users.rb +5 -3
- data/lib/icfs/users_fs.rb +8 -6
- data/lib/icfs/users_redis.rb +12 -10
- data/lib/icfs/users_s3.rb +6 -4
- data/lib/icfs/utils/backup.rb +30 -29
- data/lib/icfs/utils/check.rb +36 -34
- data/lib/icfs/validate.rb +24 -15
- data/lib/icfs/web/auth_ssl.rb +7 -9
- data/lib/icfs/web/client.rb +671 -679
- data/lib/icfs.rb +174 -10
- metadata +16 -7
- data/devel/devel-webrick.yml +0 -49
@@ -9,10 +9,11 @@
|
|
9
9
|
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
10
10
|
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
11
11
|
|
12
|
+
# frozen_string_literal: true
|
13
|
+
|
12
14
|
require_relative 'config'
|
13
15
|
|
14
16
|
module ICFS
|
15
|
-
module Web
|
16
17
|
|
17
18
|
##########################################################################
|
18
19
|
# Configuration storage implemented in S3
|
@@ -32,7 +33,7 @@ class ConfigS3 < Config
|
|
32
33
|
super(defaults)
|
33
34
|
@s3 = s3
|
34
35
|
@bck = bucket
|
35
|
-
@pre = prefix || ''
|
36
|
+
@pre = prefix || ''
|
36
37
|
end
|
37
38
|
|
38
39
|
|
@@ -40,10 +41,10 @@ class ConfigS3 < Config
|
|
40
41
|
# (see Config#load)
|
41
42
|
#
|
42
43
|
def load(unam)
|
43
|
-
Items.validate(unam, 'User/Role/Group name'
|
44
|
+
Items.validate(unam, 'User/Role/Group name', Items::FieldUsergrp)
|
44
45
|
@unam = unam.dup
|
45
46
|
json = @s3.get_object( bucket: @bck, key: _key(unam) ).body.read
|
46
|
-
@data = Items.parse(json, 'Config values'
|
47
|
+
@data = Items.parse(json, 'Config values', Config::ValConfig)
|
47
48
|
return true
|
48
49
|
rescue
|
49
50
|
@data = {}
|
@@ -55,12 +56,11 @@ class ConfigS3 < Config
|
|
55
56
|
# (see Config#save)
|
56
57
|
#
|
57
58
|
def save()
|
58
|
-
raise(RuntimeError, 'Save requires a user name'
|
59
|
-
json = Items.generate(@data, 'Config values'
|
59
|
+
raise(RuntimeError, 'Save requires a user name') if !@unam
|
60
|
+
json = Items.generate(@data, 'Config values', Config::ValConfig)
|
60
61
|
@s3.put_object( bucket: @bck, key: _key(@unam), body: json )
|
61
62
|
end # def save()
|
62
63
|
|
63
|
-
end # class ICFS::
|
64
|
+
end # class ICFS::ConfigS3
|
64
65
|
|
65
|
-
end # module ICFS::Web
|
66
66
|
end # module ICFS
|
data/lib/icfs/demo/auth.rb
CHANGED
@@ -8,6 +8,8 @@
|
|
8
8
|
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
9
9
|
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
10
10
|
|
11
|
+
# frozen_string_literal: true
|
12
|
+
|
11
13
|
require 'rack'
|
12
14
|
|
13
15
|
module ICFS
|
@@ -27,12 +29,10 @@ class Auth
|
|
27
29
|
#
|
28
30
|
# @param app [Object] The rack app
|
29
31
|
# @param api [Object] the ICFS API
|
30
|
-
# @param cfg [Object] the ICFS Web Config object
|
31
32
|
#
|
32
|
-
def initialize(app, api
|
33
|
+
def initialize(app, api)
|
33
34
|
@app = app
|
34
35
|
@api = api
|
35
|
-
@cfg = cfg
|
36
36
|
end
|
37
37
|
|
38
38
|
|
@@ -42,7 +42,7 @@ class Auth
|
|
42
42
|
def call(env)
|
43
43
|
|
44
44
|
# login
|
45
|
-
if env['PATH_INFO'] == '/login'
|
45
|
+
if env['PATH_INFO'] == '/login'
|
46
46
|
user = env['QUERY_STRING']
|
47
47
|
body = 'User set'
|
48
48
|
|
@@ -59,14 +59,12 @@ class Auth
|
|
59
59
|
cookies = Rack::Request.new(env).cookies
|
60
60
|
user = cookies['icfs-user']
|
61
61
|
if !user
|
62
|
-
return [400, {'Content-Type' => 'text/plain'}, ['Login first'
|
62
|
+
return [400, {'Content-Type' => 'text/plain'}, ['Login first']]
|
63
63
|
end
|
64
64
|
|
65
65
|
# set up for the call
|
66
66
|
@api.user = user
|
67
67
|
env['icfs'] = @api
|
68
|
-
@cfg.load(user)
|
69
|
-
env['icfs.config'] = @cfg
|
70
68
|
return @app.call(env)
|
71
69
|
|
72
70
|
rescue ICFS::Error::NotFound, ICFS::Error::Value => err
|
data/lib/icfs/demo/static.rb
CHANGED
data/lib/icfs/elastic.rb
CHANGED
@@ -9,6 +9,8 @@
|
|
9
9
|
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
10
10
|
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
11
11
|
|
12
|
+
# frozen_string_literal: true
|
13
|
+
|
12
14
|
#
|
13
15
|
module ICFS
|
14
16
|
|
@@ -28,12 +30,12 @@ module Elastic
|
|
28
30
|
# @return [String] JSON encoded object
|
29
31
|
#
|
30
32
|
def _read(ix, id)
|
31
|
-
url = '%s/_doc/%s/_source'
|
32
|
-
resp = @es.run_request(:get, url, ''
|
33
|
+
url = '%s/_doc/%s/_source' % [ @map[ix], CGI.escape(id)]
|
34
|
+
resp = @es.run_request(:get, url, '', {})
|
33
35
|
if resp.status == 404
|
34
36
|
return nil
|
35
37
|
elsif !resp.success?
|
36
|
-
raise('Elasticsearch read failed'
|
38
|
+
raise('Elasticsearch read failed')
|
37
39
|
end
|
38
40
|
return resp.body
|
39
41
|
end # def _read()
|
@@ -47,11 +49,11 @@ module Elastic
|
|
47
49
|
# @param item [String] JSON encoded object to write
|
48
50
|
#
|
49
51
|
def _write(ix, id, item)
|
50
|
-
url = '%s/_doc/%s'
|
51
|
-
head = {'Content-Type'
|
52
|
+
url = '%s/_doc/%s' % [ @map[ix], CGI.escape(id)]
|
53
|
+
head = {'Content-Type' => 'application/json'}.freeze
|
52
54
|
resp = @es.run_request(:put, url, item, head)
|
53
55
|
if !resp.success?
|
54
|
-
raise('Elasticsearch index failed'
|
56
|
+
raise('Elasticsearch index failed')
|
55
57
|
end
|
56
58
|
end # def _write()
|
57
59
|
|
@@ -64,7 +66,7 @@ module Elastic
|
|
64
66
|
# @param maps [Hash] symbol to Elasticsearch mapping
|
65
67
|
#
|
66
68
|
def create(maps)
|
67
|
-
head = {'Content-Type'
|
69
|
+
head = {'Content-Type' => 'application/json'}.freeze
|
68
70
|
maps.each do |ix, map|
|
69
71
|
url = @map[ix]
|
70
72
|
resp = @es.run_request(:put, url, map, head)
|
@@ -72,7 +74,7 @@ module Elastic
|
|
72
74
|
puts 'URL: %s' % url
|
73
75
|
puts map
|
74
76
|
puts resp.body
|
75
|
-
raise('Elasticsearch index create failed: %s'
|
77
|
+
raise('Elasticsearch index create failed: %s' % ix.to_s)
|
76
78
|
end
|
77
79
|
end
|
78
80
|
end # def create()
|
@@ -0,0 +1,242 @@
|
|
1
|
+
#
|
2
|
+
# Investigative Case File System
|
3
|
+
#
|
4
|
+
# Copyright 2019 by Graham A. Field
|
5
|
+
#
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License version 3.
|
8
|
+
#
|
9
|
+
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
10
|
+
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
11
|
+
|
12
|
+
# frozen_string_literal: true
|
13
|
+
|
14
|
+
require_relative 'core'
|
15
|
+
|
16
|
+
module ICFS
|
17
|
+
module Email
|
18
|
+
|
19
|
+
##########################################################################
|
20
|
+
# Basic email processing
|
21
|
+
#
|
22
|
+
# This looks for ICFS email gateway instructions, and processes
|
23
|
+
# attachments.
|
24
|
+
#
|
25
|
+
class Basic
|
26
|
+
|
27
|
+
###############################################
|
28
|
+
# Strip regex
|
29
|
+
StripRx = /^[^[:graph:]]*([[:graph:]].*[[:graph:]])[^[:graph:]]*$/.freeze
|
30
|
+
|
31
|
+
|
32
|
+
###############################################
|
33
|
+
# Strip spaces from collected lines
|
34
|
+
#
|
35
|
+
def _strip(collect)
|
36
|
+
collect.map{ |lr|
|
37
|
+
ma = StripRx.match(lr) # include wierd UNICODE spaces
|
38
|
+
ma ? ma[1] : nil
|
39
|
+
}.compact
|
40
|
+
end # def _strip()
|
41
|
+
|
42
|
+
|
43
|
+
###############################################
|
44
|
+
# Check for a boolean
|
45
|
+
#
|
46
|
+
def _boolean(str)
|
47
|
+
case str.downcase
|
48
|
+
when 'true', 'yes'
|
49
|
+
return true
|
50
|
+
when 'false', 'no'
|
51
|
+
return false
|
52
|
+
else
|
53
|
+
return nil
|
54
|
+
end
|
55
|
+
end # def _boolean()
|
56
|
+
|
57
|
+
|
58
|
+
###############################################
|
59
|
+
# Fields regex
|
60
|
+
FieldRx = /^ICFS ([^:[:blank:]]*)[[:blank:]]*:[[:blank:]]*(.*)[[:blank:]]*$/.freeze
|
61
|
+
|
62
|
+
|
63
|
+
###############################################
|
64
|
+
# Regex for stat
|
65
|
+
StatRx = /^([+\-]?\d+(\.\d*)?)[^[:graph:]]+([[:graph:]].*[[:graph:]])$/.freeze
|
66
|
+
|
67
|
+
|
68
|
+
###############################################
|
69
|
+
# Look for instructions in the email and process them
|
70
|
+
#
|
71
|
+
def receive(env)
|
72
|
+
|
73
|
+
# we only work on text/plain version of the email
|
74
|
+
if env[:msg].multipart?
|
75
|
+
txt = env[:msg].text_part
|
76
|
+
elsif env[:msg].mime_type == 'text/plain'
|
77
|
+
txt = env[:msg]
|
78
|
+
end
|
79
|
+
return [:continue, nil] if !txt
|
80
|
+
lines = txt.decoded.lines
|
81
|
+
|
82
|
+
# User specified values
|
83
|
+
collect = nil
|
84
|
+
term = nil
|
85
|
+
state = nil
|
86
|
+
stat_name = nil
|
87
|
+
stat_value = nil
|
88
|
+
lines.each do |ln|
|
89
|
+
# collecting lines
|
90
|
+
if collect
|
91
|
+
if ln.start_with?(term)
|
92
|
+
case state
|
93
|
+
|
94
|
+
when :tags
|
95
|
+
tags = _strip(collect)
|
96
|
+
unless tags.empty?
|
97
|
+
env[:tags] ||= []
|
98
|
+
env[:tags] = env[:tags] + tags
|
99
|
+
end
|
100
|
+
collect = nil
|
101
|
+
|
102
|
+
when :perms
|
103
|
+
perms = _strip(collect)
|
104
|
+
env[:perms] = perms unless perms.empty?
|
105
|
+
collect = nil
|
106
|
+
|
107
|
+
when :stat
|
108
|
+
credit = _strip(collect)
|
109
|
+
env[:stats] ||= []
|
110
|
+
env[:stats] << {
|
111
|
+
'name' => stat_name,
|
112
|
+
'value' => stat_value,
|
113
|
+
'credit' => credit
|
114
|
+
}
|
115
|
+
collect = nil
|
116
|
+
|
117
|
+
when :content
|
118
|
+
cont = collect.map{|lr| lr.delete("\r")}.join('')
|
119
|
+
env[:content] = cont unless cont.empty?
|
120
|
+
collect = nil
|
121
|
+
|
122
|
+
else
|
123
|
+
raise ScriptError
|
124
|
+
end
|
125
|
+
else
|
126
|
+
collect << ln
|
127
|
+
next
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
next unless ma = FieldRx.match(ln)
|
132
|
+
fn = ma[1].downcase
|
133
|
+
|
134
|
+
case fn
|
135
|
+
when 'case'
|
136
|
+
env[:caseid] = ma[2].strip
|
137
|
+
|
138
|
+
when 'entry'
|
139
|
+
enum = ma[2].strip.to_i
|
140
|
+
env[:entry] = enum if enum != 0
|
141
|
+
|
142
|
+
when 'title'
|
143
|
+
env[:title] = ma[2].strip
|
144
|
+
|
145
|
+
when 'time'
|
146
|
+
if env[:user]
|
147
|
+
env[:api].user = env[:user]
|
148
|
+
tm = ICFS.time_parse(ma[2].strip, env[:api].config)
|
149
|
+
env[:time] = tm if tm
|
150
|
+
end
|
151
|
+
|
152
|
+
when 'tag'
|
153
|
+
env[:tags] ||= []
|
154
|
+
env[:tags] << ma[2].strip
|
155
|
+
|
156
|
+
when 'tags'
|
157
|
+
collect = []
|
158
|
+
state = :tags
|
159
|
+
term = 'ICFS'
|
160
|
+
|
161
|
+
when 'perms'
|
162
|
+
collect = []
|
163
|
+
state = :perms
|
164
|
+
term = 'ICFS'
|
165
|
+
|
166
|
+
when 'stat'
|
167
|
+
next unless pm = StatRx.match(ma[2].strip)
|
168
|
+
stat_name = pm[3]
|
169
|
+
stat_value = pm[1].to_f
|
170
|
+
collect = []
|
171
|
+
state = :stat
|
172
|
+
term = 'ICFS'
|
173
|
+
|
174
|
+
when 'content'
|
175
|
+
collect = []
|
176
|
+
state = :content
|
177
|
+
term = ma[2].strip
|
178
|
+
term = 'ICFS' if term.empty?
|
179
|
+
|
180
|
+
when 'save_files'
|
181
|
+
val = _boolean(ma[2].strip)
|
182
|
+
env[:save_files] = val unless val.nil?
|
183
|
+
|
184
|
+
when 'save_original'
|
185
|
+
val = _boolean(ma[2].strip)
|
186
|
+
env[:save_original] = val unless val.nil?
|
187
|
+
|
188
|
+
when 'save_email'
|
189
|
+
val = _boolean(ma[2].strip)
|
190
|
+
env[:save_email] = val unless val.nil?
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
# time defaults to message date
|
196
|
+
unless env[:time]
|
197
|
+
env[:time] = env[:msg].date.to_time.to_i
|
198
|
+
end
|
199
|
+
|
200
|
+
# title defaults to subject if okay
|
201
|
+
unless env[:title]
|
202
|
+
# check the subject time
|
203
|
+
title = env[:msg].subject
|
204
|
+
err = Validate.check(title, Items::FieldTitle)
|
205
|
+
env[:title] = title unless err
|
206
|
+
end
|
207
|
+
|
208
|
+
# save the edited email defaults to yes
|
209
|
+
unless env.key?(:save_email)
|
210
|
+
env[:save_email] = true
|
211
|
+
end
|
212
|
+
|
213
|
+
# save the raw email defaults to no
|
214
|
+
unless env.key?(:save_original)
|
215
|
+
env[:save_original] = false
|
216
|
+
end
|
217
|
+
|
218
|
+
# save attachments as files
|
219
|
+
unless env.key?(:save_files) && !env[:save_files]
|
220
|
+
cnt = 0
|
221
|
+
env[:msg].attachments.each do |att|
|
222
|
+
type = att.header[:content_disposition].disposition_type
|
223
|
+
next if type == 'inline'
|
224
|
+
cnt += 1
|
225
|
+
name = att.filename
|
226
|
+
if !name
|
227
|
+
ext = MIME::Types[att.content_type].first.extensions.first
|
228
|
+
name = 'unnamed_%d.%s' % [cnt, ext]
|
229
|
+
end
|
230
|
+
env[:files] << { name: name, content: att.decoded }
|
231
|
+
end
|
232
|
+
env[:msg] = ::Mail.new(env[:msg].without_attachments!.encoded)
|
233
|
+
end
|
234
|
+
|
235
|
+
return :continue
|
236
|
+
end # def receive()
|
237
|
+
|
238
|
+
|
239
|
+
end # class ICFS::Email::RxCore
|
240
|
+
|
241
|
+
end # module ICFS::Email
|
242
|
+
end # module ICFS
|
@@ -0,0 +1,293 @@
|
|
1
|
+
#
|
2
|
+
# Investigative Case File System
|
3
|
+
#
|
4
|
+
# Copyright 2019 by Graham A. Field
|
5
|
+
#
|
6
|
+
# This program is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License version 3.
|
8
|
+
#
|
9
|
+
# This program is distributed WITHOUT ANY WARRANTY; without even the
|
10
|
+
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
11
|
+
|
12
|
+
# frozen_string_literal: true
|
13
|
+
|
14
|
+
require 'set'
|
15
|
+
|
16
|
+
module ICFS
|
17
|
+
|
18
|
+
|
19
|
+
##########################################################################
|
20
|
+
# Email integration with ICFS
|
21
|
+
#
|
22
|
+
module Email
|
23
|
+
|
24
|
+
|
25
|
+
##########################################################################
|
26
|
+
# Core email processing engine.
|
27
|
+
#
|
28
|
+
class Core
|
29
|
+
|
30
|
+
|
31
|
+
###############################################
|
32
|
+
# A file to attach
|
33
|
+
ValFile = {
|
34
|
+
method: :hash,
|
35
|
+
required: {
|
36
|
+
content: Validate::IsString, # content of the file
|
37
|
+
name: Items::FieldFilename, # name of the file
|
38
|
+
}.freeze
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
|
42
|
+
###############################################
|
43
|
+
# Results of processing a valid message
|
44
|
+
ValReceive = {
|
45
|
+
method: :hash,
|
46
|
+
required: {
|
47
|
+
orig: Validate::IsString, # the raw message
|
48
|
+
caseid: Items::FieldCaseid, # case to write
|
49
|
+
user: Items::FieldUsergrp, # user as author
|
50
|
+
files: { # files to attach
|
51
|
+
method: :array,
|
52
|
+
check: ValFile
|
53
|
+
}.freeze,
|
54
|
+
}.freeze,
|
55
|
+
optional: {
|
56
|
+
entry: Validate::IsIntPos, # the entry number
|
57
|
+
time: Validate::IsIntPos, # the time of the entry
|
58
|
+
title: Items::FieldTitle, # title of the entry
|
59
|
+
content: Items::FieldContent, # content of the entry
|
60
|
+
tags: { # tags to apply
|
61
|
+
method: :array,
|
62
|
+
check: Items::FieldTag
|
63
|
+
}.freeze,
|
64
|
+
perms: { # perms to apply
|
65
|
+
method: :array,
|
66
|
+
check: Items::FieldPermAny,
|
67
|
+
}.freeze,
|
68
|
+
stats: { # stats to apply
|
69
|
+
method: :array,
|
70
|
+
min: 1,
|
71
|
+
check: {
|
72
|
+
method: :hash,
|
73
|
+
required: {
|
74
|
+
"name" => Items::FieldStat,
|
75
|
+
"value" => Validate::IsFloat,
|
76
|
+
"credit" => {
|
77
|
+
method: :array,
|
78
|
+
min: 1,
|
79
|
+
max: 32,
|
80
|
+
check: Items::FieldUsergrp
|
81
|
+
}.freeze
|
82
|
+
}.freeze
|
83
|
+
}.freeze
|
84
|
+
}.freeze,
|
85
|
+
save_raw: Validate::IsBoolean, # save the raw email as a File
|
86
|
+
save_msg: Validate::IsBoolean, # save the processed message w/o attach
|
87
|
+
}.freeze,
|
88
|
+
others: true,
|
89
|
+
}.freeze
|
90
|
+
|
91
|
+
|
92
|
+
###############################################
|
93
|
+
# Default title
|
94
|
+
DefaultTitle = 'Email gateway default title'
|
95
|
+
|
96
|
+
|
97
|
+
###############################################
|
98
|
+
# Default content
|
99
|
+
DefaultContent = 'Entry generated via email gateway with no content.'
|
100
|
+
|
101
|
+
|
102
|
+
###############################################
|
103
|
+
# Filename for original content
|
104
|
+
DefaultOrig = 'email_received.eml'
|
105
|
+
|
106
|
+
|
107
|
+
###############################################
|
108
|
+
# Filename for processed content without attachments
|
109
|
+
DefaultEmail = 'email.eml'
|
110
|
+
|
111
|
+
|
112
|
+
###############################################
|
113
|
+
# New instance
|
114
|
+
#
|
115
|
+
# @param api [ICFS::Api] the ICFS API
|
116
|
+
# @param log [Logger] The log
|
117
|
+
# @param st [Array] the middleware
|
118
|
+
#
|
119
|
+
def initialize(api, log, st=nil)
|
120
|
+
@api = api
|
121
|
+
@log = log
|
122
|
+
self.stack_set(st) if st
|
123
|
+
end # def initialize()
|
124
|
+
|
125
|
+
|
126
|
+
###############################################
|
127
|
+
# Set the middleware stack
|
128
|
+
#
|
129
|
+
# Each middleware object must respond to #receive and return one of:
|
130
|
+
# * :continue - process more middleware
|
131
|
+
# * :success - stops further middleare, and records the entry
|
132
|
+
# * :failure - stops further middlware and does not record
|
133
|
+
#
|
134
|
+
def stack_set(st)
|
135
|
+
@stack = st
|
136
|
+
end # def stack_set()
|
137
|
+
|
138
|
+
|
139
|
+
###############################################
|
140
|
+
# Process a received email using the middleware stack
|
141
|
+
#
|
142
|
+
# @param msg [::Mail::Message] the email message
|
143
|
+
# @return [Array] results, first field is a Symbol, second field is error or
|
144
|
+
# the recorded message
|
145
|
+
#
|
146
|
+
def receive(msg)
|
147
|
+
@log.debug('Email: Processing %s' % msg.message_id)
|
148
|
+
|
149
|
+
# setup the environment
|
150
|
+
env = {
|
151
|
+
orig: msg.raw_source.dup, # the original text email
|
152
|
+
msg: msg, # the email message being worked on
|
153
|
+
files: [], # files to attach to the entry
|
154
|
+
api: @api, # the ICFS API
|
155
|
+
}
|
156
|
+
|
157
|
+
# process all middleware
|
158
|
+
@stack.each do |mid|
|
159
|
+
resp, err = mid.receive(env)
|
160
|
+
case resp
|
161
|
+
when :continue
|
162
|
+
next
|
163
|
+
when :stop
|
164
|
+
break
|
165
|
+
when :failure
|
166
|
+
return [:failure, err]
|
167
|
+
else
|
168
|
+
raise ScriptError
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# check that all required fields were completed
|
173
|
+
err = Validate.check(env, ValReceive)
|
174
|
+
if err
|
175
|
+
@log.info('Email: Invalid: %s' % err.inspect)
|
176
|
+
return [:invalid, err]
|
177
|
+
end
|
178
|
+
|
179
|
+
# API set to active user
|
180
|
+
@api.user = env[:user]
|
181
|
+
|
182
|
+
# if an entry was specified
|
183
|
+
if env[:entry] && env[:entry] != 0
|
184
|
+
ent = @api.entry_read(env[:caseid], env[:entry])
|
185
|
+
ent.delete('icfs')
|
186
|
+
ent.delete('log')
|
187
|
+
ent.delete('user')
|
188
|
+
ent.delete('tags') if ent['tags'][0] == ICFS::TagNone
|
189
|
+
else
|
190
|
+
ent = {}
|
191
|
+
ent['caseid'] = env[:caseid]
|
192
|
+
end
|
193
|
+
|
194
|
+
# build entry
|
195
|
+
ent['time'] = env[:time] if env[:time]
|
196
|
+
ent['title'] = env[:title] if env[:title]
|
197
|
+
ent['title'] ||= DefaultTitle
|
198
|
+
ent['content'] = env[:content] if env[:content]
|
199
|
+
ent['content'] ||= DefaultContent
|
200
|
+
if env[:tags]
|
201
|
+
ent['tags'] ||= []
|
202
|
+
ent['tags'] = (ent['tags'] + env[:tags]).uniq
|
203
|
+
end
|
204
|
+
ent['perms'] = env[:perms].uniq if env[:perms]
|
205
|
+
ent['stats'] = env[:stats] if env[:stats]
|
206
|
+
|
207
|
+
# files
|
208
|
+
files = env[:files].map do |fd|
|
209
|
+
tmp = @api.tempfile
|
210
|
+
tmp.write(fd[:content])
|
211
|
+
{ 'name' => fd[:name], 'temp' => tmp }
|
212
|
+
end
|
213
|
+
if env[:save_original]
|
214
|
+
tmp = @api.tempfile
|
215
|
+
tmp.write(env[:orig])
|
216
|
+
files << { 'name' => DefaultOrig, 'temp' => tmp }
|
217
|
+
end
|
218
|
+
if env[:save_email]
|
219
|
+
tmp = @api.tempfile
|
220
|
+
env[:msg].header.fields.delete_if do |fi|
|
221
|
+
!FieldsSet.include?(fi.name.downcase)
|
222
|
+
end
|
223
|
+
tmp.write(env[:msg].encoded)
|
224
|
+
files << { 'name' => DefaultEmail, 'temp' => tmp }
|
225
|
+
end
|
226
|
+
unless files.empty?
|
227
|
+
ent['files'] ||= []
|
228
|
+
ent['files'] = ent['files'] + files
|
229
|
+
end
|
230
|
+
|
231
|
+
# try to record it
|
232
|
+
@api.record(ent, nil, nil, nil)
|
233
|
+
|
234
|
+
@log.info('Email: Success: %s %d-%d' %
|
235
|
+
[ent['caseid'], ent['entry'], ent['log']])
|
236
|
+
return [:success, ent]
|
237
|
+
|
238
|
+
rescue ICFS::Error::Conflict => ex
|
239
|
+
@log.warn('Email: Conflict: %s' % ex.message)
|
240
|
+
return [:conflict, ex.message]
|
241
|
+
rescue ICFS::Error::NotFound => ex
|
242
|
+
@log.warn('Email: Not Found: %s' % ex.message)
|
243
|
+
return [:notfound, ex.message]
|
244
|
+
rescue ICFS::Error::Perms => ex
|
245
|
+
@log.warn('Email: Permissions: %s' % ex.message)
|
246
|
+
return [:perms, ex.message]
|
247
|
+
rescue ICFS::Error::Value => ex
|
248
|
+
@log.warn('Email: Value: %s' % ex.message)
|
249
|
+
return [:value, ex.message]
|
250
|
+
end # def receive()
|
251
|
+
|
252
|
+
|
253
|
+
###############################################
|
254
|
+
# Basic header fields to copy
|
255
|
+
#
|
256
|
+
CopyFields = [
|
257
|
+
"to",
|
258
|
+
"cc",
|
259
|
+
"message-id",
|
260
|
+
"in-reply-to",
|
261
|
+
"references",
|
262
|
+
"subject",
|
263
|
+
"comments",
|
264
|
+
"keywords",
|
265
|
+
"date",
|
266
|
+
"from",
|
267
|
+
"sender",
|
268
|
+
"reply-to",
|
269
|
+
].freeze
|
270
|
+
|
271
|
+
|
272
|
+
###############################################
|
273
|
+
# Content related fields
|
274
|
+
#
|
275
|
+
ContentFields = [
|
276
|
+
"content-transfer-encoding",
|
277
|
+
"content-description",
|
278
|
+
"content-disposition",
|
279
|
+
"content-type",
|
280
|
+
"content-id",
|
281
|
+
"content-location",
|
282
|
+
].freeze
|
283
|
+
|
284
|
+
|
285
|
+
###############################################
|
286
|
+
# Set of header fields to copy set
|
287
|
+
FieldsSet = Set.new(CopyFields).merge(ContentFields).freeze
|
288
|
+
|
289
|
+
end # class ICFS::Email::Core
|
290
|
+
|
291
|
+
|
292
|
+
end # module ICFS::Email
|
293
|
+
end # module ICFS
|