File Coverage

blib/lib/EMDIS/ECS/Message.pm
Criterion Covered Total %
statement 127 175 72.5
branch 36 74 48.6
condition 0 3 0.0
subroutine 25 26 96.1
pod 0 20 0.0
total 188 298 63.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             #
3             # Copyright (C) 2002-2020 National Marrow Donor Program. All rights reserved.
4             #
5             # For a description of this module, please refer to the POD documentation
6             # embedded at the bottom of the file (e.g. perldoc EMDIS::ECS::Message).
7              
8             package EMDIS::ECS::Message;
9              
10 1         229 use EMDIS::ECS qw($ECS_CFG $VERSION ecs_is_configured
11 1     1   14984 pgp2_decrypt openpgp_decrypt);
  1         5  
12 1     1   9 use IO::File;
  1         2  
  1         165  
13 1     1   6 use MIME::QuotedPrint qw( decode_qp );
  1         30  
  1         55  
14 1     1   6 use strict;
  1         2  
  1         42  
15 1     1   7 use vars qw($EOL_PATTERN);
  1         1  
  1         66  
16              
17             BEGIN {
18 1     1   1933 $EOL_PATTERN = "\r?\n";
19             }
20              
21             # ----------------------------------------------------------------------
22             # Constructor.
23             # If error encountered, returns error message instead of object reference.
24             sub new {
25 20     20 0 1527 my $arg1 = shift;
26 20         33 my $this;
27 20 50       42 if(ref $arg1) {
28             # invoked as instance method
29 0         0 $this = $arg1;
30             }
31             else {
32             # invoked as class method
33 20         89 $this = {};
34 20         37 bless $this, $arg1;
35             }
36              
37             # remember raw text
38 20         49 my $raw_text = shift;
39 20         99 $this->{raw_text} = $raw_text;
40              
41             # parse raw email message text
42 20         359 $raw_text =~ s/$EOL_PATTERN/\n/g; # convert to more easily parseable format
43 20 100       104 if($raw_text =~ /(.*?\n)\n(.*)/s) {
44 19         84 $this->{headers} = $1;
45 19         54 $this->{body} = $2;
46 19         39 $this->{cleartext} = '';
47             } else {
48 1         5 return "unable to parse message raw text.";
49             }
50              
51             # get "Subject" (required)
52 19 100       108 if($this->{headers} =~ /^Subject:\s*(.+?)$/im) {
53 18         50 $this->{subject} = $1;
54             } else {
55 1         4 return "message subject not found.";
56             }
57              
58             # get "x-emdis-message-type" (optional)
59 18 50       66 if($this->{headers} =~ /^x-emdis-message-type:\s*(\S+)\s*$/imo) {
60 0         0 $this->{emdis_message_type} = $1;
61             }
62              
63             # attempt to parse "Subject" into MAIL_MRK:sender[:seqnum]
64 18         27 my $mail_mrk = 'EMDIS';
65 18 50       48 if(ecs_is_configured()) {
66 0         0 $mail_mrk = $ECS_CFG->MAIL_MRK;
67             }
68             else {
69 18         214 warn "ECS not configured, using MAIL_MRK = '$mail_mrk'\n";
70             }
71 18         63 my ($mark, $sender, $seq_num);
72 18 100       200 if($this->{subject} =~ /$mail_mrk:(\S+?):(\d+)(:(\d+)\/(\d+))?\s*$/i) {
    100          
    100          
73             # regular message
74 9         20 $this->{is_ecs_message} = 1;
75 9         17 $this->{is_meta_message} = '';
76 9         26 $this->{is_document} = '';
77 9         24 $this->{sender} = $1;
78 9         20 $this->{seq_num} = $2;
79 9 100       39 $this->{part_num} = defined $4 ? $4 : 1;
80 9 100       22 $this->{num_parts} = defined $5 ? $5 : 1;
81 9 100       27 if($this->{part_num} > $this->{num_parts}) {
82 1         8 return "part_num is greater than num_parts: " . $this->{subject};
83             }
84             }
85             elsif($this->{subject} =~ /$mail_mrk:(\S+?):(\d+):DOC\s*$/io) {
86             # document
87 1         4 $this->{sender} = $1;
88 1         5 $this->{is_ecs_message} = '';
89 1         5 $this->{is_meta_message} = '';
90 1         3 $this->{is_document} = 1;
91 1         13 $this->{seq_num} = $2;
92 1         4 $this->{emdis_message_type} = 'DOC';
93             }
94             elsif($this->{subject} =~ /$mail_mrk:(\S+)\s*$/i) {
95             # meta-message
96 3         12 $this->{sender} = $1;
97 3         7 $this->{is_ecs_message} = 1;
98 3         9 $this->{is_meta_message} = 1;
99 3         7 $this->{is_document} = '';
100             }
101             else {
102             # not an ECS message
103 5         13 $this->{is_ecs_message} = '';
104 5         9 $this->{is_meta_message} = '';
105 5         13 $this->{is_document} = '';
106             }
107              
108             # get "Content-type" (optional)
109 17 100       63 if($this->{headers} =~ /^Content-type:\s*(.+?)\s*$/im) {
110 1         4 $this->{content_type} = $1;
111             }
112              
113             # get "From" (optional)
114 17 100       105 if($this->{headers} =~ /^From:\s*(.+?)\s*$/im) {
115 15         50 $this->{from} = $1;
116             }
117              
118             # get "To" (optional)
119 17 100       86 if($this->{headers} =~ /^To:\s*(.+?)\s*$/im) {
120 15         42 $this->{to} = $1;
121             }
122              
123             # decode quoted printable e-mails if necessary
124 17 100       51 if($this->{headers} =~ /^Content-Transfer-Encoding\s*:\s*quoted-printable$/im) {
125             $this->{headers} =~
126 1         11 s/^(Content-Transfer-Encoding)\s*:\s*quoted-printable$/$1: 8bit/im;
127 1         10 $this->{body} = decode_qp($this->{body});
128             }
129              
130 17         73 return $this;
131             }
132              
133             # ----------------------------------------------------------------------
134             # Accessor method (read-only).
135             sub content_type {
136 4     4 0 10 my $this = shift;
137 4         19 return $this->{content_type};
138             }
139              
140             # ----------------------------------------------------------------------
141             # Accessor method (read-only).
142             sub cleartext {
143 5     5 0 195 my $this = shift;
144 5         32 return $this->{cleartext};
145             }
146              
147             # ----------------------------------------------------------------------
148             # Accessor method (read-only).
149             sub body {
150 5     5 0 71 my $this = shift;
151 5         20 return $this->{body};
152             }
153              
154             # ----------------------------------------------------------------------
155             # Accessor method (read-only).
156             sub from {
157 6     6 0 13 my $this = shift;
158 6         38 return $this->{from};
159             }
160              
161             # ----------------------------------------------------------------------
162             # Accessor method (read-only).
163             sub headers {
164 4     4 0 7 my $this = shift;
165 4         15 return $this->{headers};
166             }
167              
168             # ----------------------------------------------------------------------
169             # Accessor method (read-only).
170             sub is_document {
171 1     1 0 61 my $this = shift;
172 1         5 return $this->{is_document};
173             }
174              
175             # ----------------------------------------------------------------------
176             # Accessor method (read-only).
177             sub is_ecs_message {
178 13     13 0 520 my $this = shift;
179 13         54 return $this->{is_ecs_message};
180             }
181              
182             # ----------------------------------------------------------------------
183             # Accessor method (read-only).
184             sub is_meta_message {
185 10     10 0 18 my $this = shift;
186 10         37 return $this->{is_meta_message};
187             }
188              
189             # ----------------------------------------------------------------------
190             # Accessor method (read-only).
191             sub num_parts {
192 2     2 0 14 my $this = shift;
193 2         10 return $this->{num_parts};
194             }
195              
196             # ----------------------------------------------------------------------
197             # Accessor method (read-only).
198             sub part_num {
199 2     2 0 7 my $this = shift;
200 2         9 return $this->{part_num};
201             }
202              
203             # ----------------------------------------------------------------------
204             # Accessor method (read-only).
205             sub raw_text {
206 4     4 0 82 my $this = shift;
207 4         22 return $this->{raw_text};
208             }
209              
210             # ----------------------------------------------------------------------
211             # Accessor method (read-only).
212             sub sender {
213 9     9 0 21 my $this = shift;
214 9         35 return $this->{sender};
215             }
216              
217             # ----------------------------------------------------------------------
218             # Accessor method (read-only).
219             sub seq_num {
220 7     7 0 15 my $this = shift;
221 7         33 return $this->{seq_num};
222             }
223              
224             # ----------------------------------------------------------------------
225             # Accessor method (read-only).
226             sub subject {
227 5     5 0 60 my $this = shift;
228 5         21 return $this->{subject};
229             }
230              
231             # ----------------------------------------------------------------------
232             # Accessor method (read-only).
233             sub to {
234 6     6 0 13 my $this = shift;
235 6         22 return $this->{to};
236             }
237              
238             # ----------------------------------------------------------------------
239             # Accessor method (read-only).
240             sub full_msg {
241 2     2 0 6 my $this = shift;
242 2         34 return $this->{headers} . "\n" . $this->{body};
243             }
244              
245             # ----------------------------------------------------------------------
246             # save raw message to file
247             # returns empty string if successful, otherwise returns error message
248             sub save_to_file
249             {
250 1     1 0 65 my $err = '';
251 1         3 my $arg1 = shift;
252 1         2 my ($filename,$msg);
253 1 50       4 if(ref $arg1) {
254             # invoked as instance method
255 1         2 $msg = $arg1;
256 1         3 $filename = shift;
257             }
258             else {
259             # invoked as class method
260 0         0 $filename = $arg1;
261 0         0 my $raw_text = shift;
262 0         0 $msg = new EMDIS::ECS::Message($raw_text);
263             }
264 1 50       126 open MSGFILE, ">$filename"
265             or return "Unable to create file $filename: $!";
266 1 50       8 print MSGFILE $msg->full_msg()
267             or $err = "Unable to write file $filename: $!";
268 1         49 close MSGFILE;
269 1         33 chmod $EMDIS::ECS::FILEMODE, $filename;
270 1         10 return $err; # return error message (if any)
271             }
272              
273             # ----------------------------------------------------------------------
274             # read message from file
275             # returns object reference if successful, otherwise returns error message
276             sub read_from_file
277             {
278 1     1 0 4 my $err = '';
279 1         2 my $arg1 = shift;
280 1         2 my ($filename,$raw_text,$this);
281 1 50       4 if(ref $arg1) {
282             # invoked as instance method
283 0         0 $this = $arg1;
284 0         0 $filename = shift;
285             }
286             else {
287             # invoked as class method
288 1         2 $filename = $arg1;
289             }
290              
291             # read file
292 1 50       36 open(MSGFILE, "< $filename")
293             or return "Unable to open file $filename: $!";
294 1 50       45 $raw_text = join('', )
295             or $err = "Unable to read file $filename: $!";
296 1         19 close MSGFILE;
297 1 50       5 return $err if $err; # return error message (if any)
298              
299             # attempt to construct object
300 1         3 my $newmsg;
301 1 50       4 if(ref $arg1) {
302 0         0 $newmsg = $this->new($raw_text); # re-define this object
303             }
304             else {
305 1         5 $newmsg = new EMDIS::ECS::Message($raw_text);
306             }
307              
308             # set 'cleartext' attribute of message object
309             $newmsg->{cleartext} = $newmsg->{body}
310 1 50       6 if ref $newmsg;
311              
312 1         5 return $newmsg;
313             }
314              
315             # ----------------------------------------------------------------------
316             # read and decrypt message from encrypted file
317             # returns object reference if successful, otherwise returns error message
318             sub read_from_encrypted_file
319             {
320 0     0 0   my $err = '';
321 0           my $arg1 = shift;
322 0           my ($filename,$raw_text,$this);
323 0 0         if(ref $arg1) {
324             # invoked as instance method
325 0           $this = $arg1;
326 0           $filename = shift;
327             }
328             else {
329             # invoked as class method
330 0           $filename = $arg1;
331             }
332              
333             # read encrypted file
334 0           my $newmsg = read_from_file($filename);
335 0 0         return $newmsg unless ref $newmsg; # check for error
336 0 0 0       return "not an ECS message" unless $newmsg->is_ecs_message or $newmsg->is_document;
337              
338             # read relevant node info from node_tbl
339 0           my $node_tbl = $main::ECS_NODE_TBL;
340 0           my $was_locked = $node_tbl->LOCK;
341 0 0         if(not $was_locked) {
342 0 0         $node_tbl->lock() # lock node_tbl
343             or return "unable to lock node_tbl: " .
344             $node_tbl->ERROR;
345             }
346 0           my $node = $node_tbl->read($newmsg->sender);
347 0 0         if(not $was_locked) {
348 0           $node_tbl->unlock(); # unlock node_tbl
349             }
350 0 0         if(not $node) {
351 0           return "node not found: " . $newmsg->sender;
352             }
353              
354             # decrypt message into temp file
355 0           my $decr_filename = "$filename.asc";
356 0           for ($node->{encr_typ}) {
357 0 0         /PGP2/i and do {
358             $err = pgp2_decrypt(
359             $filename,
360             $decr_filename,
361             (/verify/i ? $node->{encr_sig} : undef),
362 0 0         $node->{encr_out_passphrase});
363 0           last;
364             };
365 0 0         /OpenPGP/i and do {
366             $err = openpgp_decrypt(
367             $filename,
368             $decr_filename,
369             (/verify/i ? $node->{encr_sig} : undef),
370 0 0         $node->{encr_out_passphrase});
371 0           last;
372             };
373 0           $err = "unrecognized encr_typ: $node->{encr_typ}\n";
374             }
375 0 0         if($err) {
376 0           unlink $decr_filename;
377 0           chomp($err);
378 0           return $err;
379             }
380              
381             # read message cleartext from temp file
382 0           my $fh = new IO::File;
383 0 0         return "unable to open file: $decr_filename"
384             unless $fh->open("< $decr_filename");
385 0           my @cleartext = $fh->getlines();
386 0           close $fh;
387              
388             # remove temp file
389 0           unlink $decr_filename;
390              
391             # set 'cleartext' message attribute
392 0           $newmsg->{cleartext} = join('', @cleartext);
393              
394 0           return $newmsg;
395             }
396              
397              
398             1;
399              
400             __DATA__