icfs 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|