MatrixBerryCore
EmailLogger.m
Go to the documentation of this file.
1 classdef EmailLogger<handle
2  properties (Access=private,Hidden)
3  emailDistributionList={};
4  emailAttachmentNameList={};
5  emailAttachmentZippedNameList={};
6  userName='unknown';
7  hostName='unknown';
8  subjectSuffix='';
9  loggerName='';
10  isDryRun=false;
11  isThrowExceptions=false;
12  %
13  smtpUserName=''
14  smtpPassword=''
15  fromEmailAddress=''
16  smtpServer='';
17 
18  end
19  methods (Access=private)
20  sendmail(self,to,subject,message,attachments)
21  end
22  methods
23  function self=EmailLogger(varargin)
24  logger=mxberry.log.log4j.Log4jConfigurator.getLogger();
25  [~,prop]=parseparams(varargin);
26  nProp=length(prop);
27  isZippedSpecified=false;
28  isFromAddressSpec=false;
29  for k=1:2:nProp
30  switch lower(prop{k})
31  case 'emaildistributionlist'
32  self.emailDistributionList=prop{k+1};
33  case 'emailattachmentnamelist'
34  self.emailAttachmentNameList=prop{k+1};
35  case 'emailattachmentzippednamelist'
36  self.emailAttachmentZippedNameList=prop{k+1};
37  isZippedSpecified=true;
38  case 'smtpserver'
39  self.smtpServer=prop{k+1};
40  case 'subjectsuffix'
41  self.subjectSuffix=prop{k+1};
42  case 'smtppassword'
43  self.smtpPassword=prop{k+1};
44  case 'smtpusername'
45  self.smtpUserName=prop{k+1};
46  case 'loggername'
47  self.loggerName=prop{k+1};
48  case 'dryrun'
49  self.isDryRun=prop{k+1};
50  case 'isthrowexceptions'
51  self.isThrowExceptions = prop{k+1};
52  case 'fromemailaddress'
53  self.fromEmailAddress=prop{k+1};
54  isFromAddressSpec=true;
55  otherwise
56  mxberry.core.throwerror('wrongInput',...
57  'unknown property %s',prop{k});
58  end
59  end
60  if isZippedSpecified
61  if numel(self.emailAttachmentNameList)~=...
62  numel(self.emailAttachmentZippedNameList)
63  mxberry.core.throwerror('wrongInput',[...
64  'properties emailAttachmentNameList and '...
65  'emailAttachmentZippedNameList are not '...
66  'consistent in length']);
67  end
68  else
69  self.emailAttachmentZippedNameList=...
70  cell(size(self.emailAttachmentNameList));
71  end
72  %% Configure email notification
73  if ~self.isDryRun&&~isFromAddressSpec
74  [curUserName,curHostName]=mxberry.system.getuserhost();
75  if ~isempty(curUserName)
76  self.userName = curUserName;
77  end
78  if ~isempty(curHostName)
79  self.hostName = curHostName;
80  end
81  self.fromEmailAddress=[self.userName,'@',self.hostName];
82  logger.info(...
83  ['no dry run mode, configured for smtpServer=',...
84  self.smtpServer]);
85  else
86  logger.info('configured for dry run');
87  end
88  end
89  function sendMessage(self,subjectMessage,varargin)
90  import mxberry.core.throwerror;
91  import mxberry.core.parseparext;
92  import mxberry.core.cell.cell2tablestr;
93  %% Create log4j logger
95  %
96  if ~self.isDryRun
97  [reg,~,attachNameList]=parseparext(varargin,...
98  {'emailAttachmentNameList';{};'iscellofstring(x)'},...
99  [0 1],'regDefList',{[]});
100  bodyMessage=reg{1};
101  %
102  emailSubjectPrefix=['[',self.loggerName,']:'];
103  emailSubjectSuffix=[self.subjectSuffix ,...
104  ', running on host:',self.hostName,'(user:',self.userName,'),',...
105  'matlab:',version('-release'),'(arch:',computer,')'];
106  emailSubject=[emailSubjectPrefix,subjectMessage,...
107  emailSubjectSuffix];
108  emailAttachNameList=self.emailAttachmentNameList;
109  emailAttachZippedNameList=self.emailAttachmentZippedNameList;
110  for iElem=1:numel(emailAttachNameList)
111  zippedNameStr=emailAttachZippedNameList{iElem};
112  if ~isempty(zippedNameStr)
113  zip(zippedNameStr,emailAttachNameList{iElem});
114  emailAttachNameList{iElem}=zippedNameStr;
115  end
116  end
117  attachNameList=[attachNameList,...
118  emailAttachNameList];
119  %
120  nAttachemments=length(attachNameList);
121  for iFile=1:nAttachemments
122  fileName=attachNameList{iFile};
123  if ~mxberry.io.isfile(fileName)
124  throwerror('wrongInput',...
125  'cannot find attachment %s',fileName);
126  end
127  end
128  %
129  logger.info(emailSubject);
130  try
131  self.sendmail(self.emailDistributionList,...
132  emailSubject,...
133  bodyMessage,...
134  attachNameList);
135  catch causeObj
136  meObj=throwerror('sendEmailFailed',...
137  ['something is wrong with the following data: \n',...
138  'distributionList: %s \n',...
139  'subject: %s\n',...
140  'smtpServer: %s\n',...
141  'emailAttachmentNameList: %s'],...
142  cell2tablestr([],self.emailDistributionList,',',...
143  'isMatlabSyntax',true),...
144  emailSubject,...
145  self.smtpServer,...
146  cell2tablestr([],self.emailAttachmentNameList,',',...
147  'isMatlabSyntax',true));
148  meObj=addCause(meObj,causeObj);
149  logger.fatal(sprintf('%s\nMessage body:\n%s',...
150  mxberry.core.MExceptionUtils.me2PlainString(meObj),...
151  bodyMessage));
152  if self.isThrowExceptions
153  throw(meObj);
154  end
155  end
156  end
157  end
158  function fromEmailAddress=getFromEmailAddress(self)
159  fromEmailAddress=self.fromEmailAddress;
160  end
161  function smtpServer=getSMTPServer(self)
162  smtpServer=self.smtpServer;
163  end
164  function suffixStr = getSubjectSuffix(self)
165  suffixStr = self.subjectSuffix;
166  end
167  function setSubjectSuffix(self, suffixStr)
168  self.subjectSuffix = suffixStr;
169  end
170  function addSubjectSuffix(self,suffixStr,isAddedToEnd)
171  if nargin<3
172  isAddedToEnd=true;
173  end
174  %
175  if isAddedToEnd
176  self.subjectSuffix=[self.subjectSuffix,suffixStr];
177  else
178  self.subjectSuffix=[suffixStr,self.subjectSuffix];
179  end
180  end
181  end
182 end
183 function sendmail(self,to,subject,message,attachments)
184 import mxberry.core.throwerror;
185 import javax.mail.*
186 import javax.mail.internet.*
187 import javax.activation.*
188 %
189 if ~usejava('jvm')
190  throwerror('wrongState:NoJvm','no Java is running');
191 end
192 
193 % Argument parsing.
194 narginchk(3,5);
195 if ischar(to)
196  to = {to};
197 end
198 if (nargin < 3)
199  message = '';
200 end
201 if (nargin < 4)
202  attachments = [];
203 elseif ischar(attachments)
204  attachments = {attachments};
205 end
206 
207 % Determine server and from.
208 [server,from] = getServerAndFrom(self);
209 if isempty(server)
210  throwerror('wrongState:SMTPServerIndeterminate',...
211  'Could not determine SMTP server');
212 end
213 if isempty(from)
214  throwerror('wrongState:FromAddressIndeterminate',...
215  'Could not determine FROM address');
216 end
217 %
218 % Use the system properties, but clone them so we don't alter them.
219 props = java.lang.System.getProperties.clone;
220 props.put('mail.smtp.host',server);
221 %
222 % Create session.
223 username = self.smtpUserName;
224 password = self.smtpPassword;
225 if isempty(username)
226  pa = [];
227 else
228  pa = com.mathworks.util.PasswordAuthenticator(username,password);
229 end
230 session = Session.getInstance(props,pa);
231 
232 % Create the message.
233 msg = MimeMessage(session);
234 
235 % Set sender.
236 msg.setFrom(getInternetAddress(from));
237 
238 % Set recipients.
239 for i = 1:numel(to)
240  msg.addRecipient(getRecipientTypeTo(msg), ...
241  getInternetAddress(to{i}));
242 end
243 
244 % Try to do the right thing on Japanese machines.
245 isJapanese = ispc && strncmpi(get(0,'Language'),'ja',2);
246 
247 % Set subject.
248 if any(subject == char(10)) || any(subject == char(13))
249  throwerror('wrongInput:InvalidSubject', ...
250  'Subjects cannot contain newline characters');
251 end
252 if isJapanese
253  msg.setSubject(subject,'SJIS')
254 else
255  msg.setSubject(subject)
256 end
257 
258 % Set other headers.
259 msg.setHeader('X-Mailer', ['MATLAB ' version])
260 msg.setSentDate(java.util.Date);
261 
262 % Construct the body of the message and attachments.
263 body = formatText(message);
264 if numel(attachments) == 0
265  if isJapanese
266  msg.setText(body,'SJIS');
267  else
268  msg.setText(body);
269  end
270 else
271  % Add body text.
272  messageBodyPart = MimeBodyPart;
273  if isJapanese
274  messageBodyPart.setText(body,'SJIS');
275  else
276  messageBodyPart.setText(body);
277  end
278  multipart = MimeMultipart;
279  multipart.addBodyPart(messageBodyPart);
280 
281  % Add attachments.
282  for iAttachments = 1:numel(attachments)
283  file = attachments{iAttachments};
284  messageBodyPart = MimeBodyPart;
285  fullName = locateFile(file);
286  if isempty(fullName)
287  throwerror('wrongState:CannotOpenFile', ...
288  'Cannot open file "%s".',file);
289  end
290  source = FileDataSource(fullName);
291  messageBodyPart.setDataHandler(DataHandler(source));
292 
293  % Remove the directory, if any, from the attachement name.
294  [~,fileName,fileExt] = fileparts(fullName);
295  messageBodyPart.setFileName([fileName fileExt]);
296  multipart.addBodyPart(messageBodyPart);
297  end
298 
299  % Put parts in message
300  msg.setContent(multipart);
301 
302 end
303 
304 % Send the message.
305 try
306  Transport.send(msg);
307 catch exception
308  % Try to make the Java error friendlier.
309  niceError = stripJavaError(exception.message);
310  if isempty(niceError)
311  throw(exception);
312  else
313  throwerror('wrongState:SmtpError','%s',niceError);
314  end
315 end
316 
317 function [server,from] = getServerAndFrom(self)
318 
319 % Check preferences.
320 server = self.smtpServer;
321 from = self.fromEmailAddress;
322 
323 % Check Java properties.
324 if isempty(server)
325  props = java.lang.System.getProperties;
326  server = char(props.getProperty('mail.smtp.host'));
327 end
328 
329 % Determine defaultMailAccountRegistry.
330 if (ispc && (isempty(server) || isempty(from)))
331  try
332  defaultMailAccount = winqueryreg('HKEY_CURRENT_USER', ...
333  'Software\Microsoft\Internet Account Manager', ...
334  'Default Mail Account');
335  defaultMailAccountRegistry = ...
336  ['Software\Microsoft\Internet Account Manager\Accounts\' ...
337  defaultMailAccount];
338  catch exception %#ok
339  defaultMailAccountRegistry = '';
340  end
341 end
342 
343 % Determine SERVER
344 if ispc && isempty(server) && ~isempty(defaultMailAccountRegistry)
345  try
346  server = winqueryreg('HKEY_CURRENT_USER',defaultMailAccountRegistry, ...
347  'SMTP Server');
348  catch exception %#ok
349  end
350 end
351 if isempty(server)
352  server = getenv('MAILHOST');
353 end
354 
355 % Determine FROM
356 if ispc && isempty(from)
357  try
358  from = winqueryreg('HKEY_CURRENT_USER',defaultMailAccountRegistry, ...
359  'SMTP Email Address');
360  catch exception %#ok
361  end
362 end
363 if isempty(from)
364  from = getenv('LOGNAME');
365 end
366 
367 
368 function internetAddress = getInternetAddress(from)
369 import mxberry.core.throwerror;
370 try
371  internetAddress = javax.mail.internet.InternetAddress(from);
372 catch meObj
373  throwerror('wrongInput:AddressError','%s',...
374  stripJavaError(meObj.message));
375 end
376 
377 
378 
379 function recipientTypeTo = getRecipientTypeTo(msg)
380 
381 % Get the class loader for the Message class.
382 cl = msg.getClass.getClassLoader;
383 % Returns a Class object pointing to RecipientType using that ClassLoader.
384 rt = java.lang.Class.forName('javax.mail.Message$RecipientType', false, cl);
385 % Returns a Field object pointint to TO.
386 field = rt.getField('TO');
387 % Gets the static instance of TO.
388 recipientTypeTo = field.get([]);
389 
390 
391 function fullPathToFile = locateFile(file)
392 
393 % Matthew J. Simoneau, November 2003
394 
395 % Checking that the length is exactly one in the first two checks automatically
396 % excludes directories, since directory listings always include '.' and '..'.
397 
398 if (length(dir(fullfile(pwd,file))) == 1)
399  % Relative path.
400  fullPathToFile = fullfile(pwd,file);
401 elseif (length(dir(file)) == 1)
402  % Absolute path.
403  fullPathToFile = file;
404 elseif ~isempty(which(file))
405  % A file on the path.
406  fullPathToFile = which(file);
407 elseif ~isempty(which([file '.']))
408  % A file on the path without extension.
409  fullPathToFile = which([file '.']);
410 else
411  fullPathToFile = '';
412 end
413 
414 
415 function toSend = formatText(msgText)
416 
417 cr = char(10);
418 
419 % For a cell array, send each cell as one line.
420 if iscell(msgText)
421  toSend = strjoin(msgText,cr);
422  return
423 end
424 
425 % For a char array, break each line at a char(10) or try to wrap to 75
426 % characters.
427 lines = {};
428 maxLineLength = 75;
429 msgText = [cr msgText cr];
430 crList = find(msgText == cr);
431 
432 for i = 1:length(crList)-1
433  nextLine = msgText(crList(i)+1 : crList(i+1)-1);
434  lineLength = length(nextLine);
435 
436  nextStart = 1;
437  moreOnLine = true;
438  while moreOnLine
439  start = nextStart;
440  if (lineLength-start+1 <= maxLineLength)
441  % The rest fits on one line.
442  stop = lineLength;
443  moreOnLine = false;
444  else
445  % Whole line doesn't fit. Needs to be broken up.
446  spaces = find(nextLine == ' ');
447  spaces = spaces(spaces >= start);
448  nonSpaces = find(nextLine ~= ' ');
449  nonSpaces = nonSpaces(nonSpaces >= start);
450  if isempty(spaces)
451  % % No spaces anywhere. Chop!
452  % stop = start+maxLineLength-1;
453  % No spaces anywhere. Preserve.
454  stop = lineLength;
455  elseif isempty(nonSpaces)
456  % Nothing but spaces. Send an empty line.
457  stop = start-1;
458  elseif (min(spaces) > (start+maxLineLength))
459  % % The first space doesn't show up soon enough to help. Chop!
460  % stop = start+maxLineLength-1;
461  % No spaces anywhere. Preserve.
462  stop = lineLength;
463  elseif isempty(spaces( ...
464  spaces > min(nonSpaces) & spaces < start+maxLineLength ...
465  ))
466  % % There are only leading spaces, which we respect. Chop!
467  % stop = start+maxLineLength-1;
468  % No spaces anywhere. Preserve.
469  stop = lineLength;
470  else
471  % Break on the last space that will make the line fit.
472  stop = max(spaces(spaces <= (start+maxLineLength)))-1;
473  end
474  % After a break, start the next line on the next non-space.
475  nonSpaces = find(nextLine ~= ' ');
476  nextStart = min(nonSpaces(nonSpaces > stop));
477  if isempty(nextStart)
478  moreOnLine = false;
479  end
480  end
481  lines{end+1,1} = nextLine(start:stop); %#ok<AGROW>
482  end
483 end
484 
485 toSend = strjoin(lines,cr);
486 
487 
488 function niceError = stripJavaError(err)
489 
490 % Two nice error messages. Pull them out and stick them together.
491 pat = 'Java exception occurred:\s*\S+: (.*?)\n\s*nested exception is:\s*\S+: (.*?)\n';
492 m = regexp(err,pat,'tokens','once');
493 if ~isempty(m)
494  niceError = sprintf('%s\n%s',m{:});
495  return
496 end
497 
498 % One nice error message. Strip it off.
499 pat = 'Java exception occurred:\s*\S+: (.*?)\n';
500 m = regexp(err,pat,'tokens','once');
501 if ~isempty(m)
502  niceError = m{1};
503  return
504 end
505 
506 % Only exceptions. Special-case popular ones.
507 pat = 'Java exception occurred:\s*(\S+)\s*\n';
508 m = regexp(err,pat,'tokens','once');
509 if ~isempty(m)
510  switch m{1}
511  case 'javax.mail.AuthenticationFailedException'
512  niceError = 'Authentication failed.';
513  return
514  end
515 end
516 
517 % Can't find a nice message.
518 niceError = '';
LOG4JCONFIGURATOR simplifies log4j configuration, especially when Parallel Computing Toolbox is used...
static function getLogger(in loggerName, in isSuffix)
GETLOGGER - gets logger for caller (it may be either script or function or method of some class) ...
function throwerror(in msgTag, in varargin)
THROWERROR works similarly to built-in ERROR function in case when there is no output arguments but s...
function parseparext(in args, in propNameValMat, in varargin)
PARSEPAREXT behaves in the same way as mxberry.core.parseparams but returns property values in a more...
function cell2tablestr(in titleList, in dataCell, in colSepSeq, in varargin)
CELL2TABLESTR - converts a cell array into a table-like char array (or a cell array of strings repres...
function parseparams(in args, in propNameList, in nRegExpected, in nPropExpected)
PARSEPARAMS behaves exactly as a built-in Matlab function apart from the incorrect behavior of Matlab...