1/* SOGoMailer.m - this file is part of SOGo
2 *
3 * Copyright (C) 2007-2015 Inverse inc.
4 *
5 * This file is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
8 * any later version.
9 *
10 * This file is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; see the file COPYING.  If not, write to
17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
19 */
20
21#import <Foundation/NSArray.h>
22#import <Foundation/NSEnumerator.h>
23#import <Foundation/NSException.h>
24#import <Foundation/NSString.h>
25
26#import <NGObjWeb/NSException+HTTP.h>
27#import <NGExtensions/NSObject+Logs.h>
28#import <NGExtensions/NSURL+misc.h>
29#import <NGMail/NGSendMail.h>
30#import <NGMail/NGSmtpClient.h>
31#import <NGMime/NGMimePartGenerator.h>
32#import <NGStreams/NGInternetSocketAddress.h>
33
34#import "NSString+Utilities.h"
35#import "SOGoAuthenticator.h"
36#import "SOGoDomainDefaults.h"
37#import "SOGoStaticAuthenticator.h"
38#import "SOGoSystemDefaults.h"
39#import "SOGoUser.h"
40#import "SOGoUserManager.h"
41
42#import "SOGoMailer.h"
43
44//
45// Useful extension that comes from Pantomime which is also
46// released under the LGPL. We should eventually merge
47// this with the same category found in SOPE's NGSmtpClient.m
48// or simply drop sope-mime in favor of Pantomime
49//
50@interface NSMutableData (DataCleanupExtension)
51
52- (unichar) characterAtIndex: (int) theIndex;
53- (NSRange) rangeOfCString: (const char *) theCString;
54- (NSRange) rangeOfCString: (const char *) theCString
55		  options: (unsigned int) theOptions
56		    range: (NSRange) theRange;
57@end
58
59@implementation NSMutableData (DataCleanupExtension)
60
61- (unichar) characterAtIndex: (int) theIndex
62{
63  const char *bytes;
64  int i, len;
65
66  len = [self length];
67
68  if (len == 0 || theIndex >= len)
69    {
70      [[NSException exceptionWithName: NSRangeException
71                    reason: @"Index out of range."
72                    userInfo: nil] raise];
73
74      return (unichar)0;
75    }
76
77  bytes = [self bytes];
78
79  for (i = 0; i < theIndex; i++)
80    {
81      bytes++;
82    }
83
84  return (unichar)*bytes;
85}
86
87- (NSRange) rangeOfCString: (const char *) theCString
88{
89  return [self rangeOfCString: theCString
90	       options: 0
91	       range: NSMakeRange(0,[self length])];
92}
93
94-(NSRange) rangeOfCString: (const char *) theCString
95		  options: (unsigned int) theOptions
96		    range: (NSRange) theRange
97{
98  const char *b, *bytes;
99  int i, len, slen;
100
101  if (!theCString)
102    {
103      return NSMakeRange(NSNotFound,0);
104    }
105
106  bytes = [self bytes];
107  len = [self length];
108  slen = strlen(theCString);
109
110  b = bytes;
111
112  if (len > theRange.location + theRange.length)
113    {
114      len = theRange.location + theRange.length;
115    }
116
117  if (theOptions == NSCaseInsensitiveSearch)
118    {
119      i = theRange.location;
120      b += i;
121
122      for (; i <= len-slen; i++, b++)
123	{
124	  if (!strncasecmp(theCString,b,slen))
125	    {
126	      return NSMakeRange(i,slen);
127	    }
128	}
129    }
130  else
131    {
132      i = theRange.location;
133      b += i;
134
135      for (; i <= len-slen; i++, b++)
136	{
137	  if (!memcmp(theCString,b,slen))
138	    {
139	      return NSMakeRange(i,slen);
140	    }
141	}
142    }
143
144  return NSMakeRange(NSNotFound,0);
145}
146
147@end
148
149@implementation SOGoMailer
150
151+ (SOGoMailer *) mailerWithDomainDefaults: (SOGoDomainDefaults *) dd
152{
153  return [[self alloc] initWithDomainDefaults: dd];
154}
155
156- (id) initWithDomainDefaults: (SOGoDomainDefaults *) dd
157{
158  if ((self = [self init]))
159    {
160      ASSIGN (mailingMechanism, [dd mailingMechanism]);
161      ASSIGN (smtpServer, [dd smtpServer]);
162      ASSIGN (authenticationType,
163              [[dd smtpAuthenticationType] lowercaseString]);
164    }
165
166  return self;
167}
168
169- (id) init
170{
171  if ((self = [super init]))
172    {
173      mailingMechanism = nil;
174      smtpServer = nil;
175      authenticationType = nil;
176    }
177
178  return self;
179}
180
181- (void) dealloc
182{
183  [mailingMechanism release];
184  [smtpServer release];
185  [authenticationType release];
186  [super dealloc];
187}
188
189- (NSException *) _sendmailSendData: (NSData *) mailData
190		       toRecipients: (NSArray *) recipients
191			     sender: (NSString *) sender
192{
193  NSException *result;
194  NGSendMail *mailer;
195
196  mailer = [NGSendMail sharedSendMail];
197  if ([mailer isSendMailAvailable])
198    result = [mailer sendMailData: mailData
199		     toRecipients: recipients
200		     sender: sender];
201  else
202    result = [NSException exceptionWithHTTPStatus: 500
203			  reason: @"cannot send message:"
204			  @" no sendmail binary!"];
205
206  return result;
207}
208
209- (NSException *) _sendMailData: (NSData *) mailData
210		     withClient: (NGSmtpClient *) client
211{
212  NSException *result;
213
214  if ([client sendData: mailData])
215    result = nil;
216  else
217    result = [NSException exceptionWithHTTPStatus: 500
218			  reason: @"cannot send message:"
219			  @" (smtp) failure when sending data"];
220
221  return result;
222}
223
224- (NSException *) _smtpSendData: (NSData *) mailData
225                   toRecipients: (NSArray *) recipients
226                         sender: (NSString *) sender
227              withAuthenticator: (id <SOGoAuthenticator>) authenticator
228                      inContext: (WOContext *) woContext
229{
230  NSString *currentTo, *login, *password;
231  NSMutableArray *toErrors;
232  NSEnumerator *addresses;
233  NGSmtpClient *client;
234  NSException *result;
235  NSURL * smtpUrl;
236
237  result = nil;
238
239  smtpUrl = [[[NSURL alloc] initWithString: smtpServer] autorelease];
240
241  client = [NGSmtpClient clientWithURL: smtpUrl];
242
243  NS_DURING
244    {
245      [client connect];
246      if ([authenticationType isEqualToString: @"plain"])
247        {
248          /* XXX Allow static credentials by peeking at the classname */
249          if ([authenticator isKindOfClass: [SOGoStaticAuthenticator class]])
250            login = [(SOGoStaticAuthenticator *)authenticator username];
251          else
252            login = [[SOGoUserManager sharedUserManager]
253                       getExternalLoginForUID: [[authenticator userInContext: woContext] loginInDomain]
254                                     inDomain: [[authenticator userInContext: woContext] domain]];
255
256          password = [authenticator passwordInContext: woContext];
257          if ([login length] == 0
258              || [login isEqualToString: @"anonymous"]
259              || ![client plainAuthenticateUser: login
260                                   withPassword: password])
261            result = [NSException
262                           exceptionWithHTTPStatus: 500
263                                            reason: @"cannot send message:"
264                       @" (smtp) authentication failure"];
265        }
266      else if (authenticationType)
267        result = [NSException
268                   exceptionWithHTTPStatus: 500
269                   reason: @"cannot send message:"
270                   @" unsupported authentication method"];
271      if (!result)
272        {
273          if ([client mailFrom: sender])
274            {
275              toErrors = [NSMutableArray array];
276              addresses = [recipients objectEnumerator];
277              currentTo = [addresses nextObject];
278              while (currentTo)
279                {
280                  if (![client recipientTo: [currentTo pureEMailAddress]])
281                    {
282                      [self logWithFormat: @"error with recipient '%@'", currentTo];
283                      [toErrors addObject: [currentTo pureEMailAddress]];
284                    }
285                  currentTo = [addresses nextObject];
286                }
287              if ([toErrors count] == [recipients count])
288                result = [NSException exceptionWithHTTPStatus: 500
289                                                       reason: @"cannot send message:"
290                                      @" (smtp) all recipients discarded"];
291              else if ([toErrors count] > 0)
292                result = [NSException exceptionWithHTTPStatus: 500
293                                                       reason: [NSString stringWithFormat:
294                                                                           @"cannot send message (smtp) - recipients discarded:\n%@",
295                                                                         [toErrors componentsJoinedByString: @", "]]];
296              else
297                result = [self _sendMailData: mailData withClient: client];
298            }
299          else
300            result = [NSException
301                       exceptionWithHTTPStatus: 500
302                                        reason: @"cannot send message: (smtp) originator not accepted"];
303        }
304      [client quit];
305      [client disconnect];
306    }
307  NS_HANDLER
308    {
309      [self errorWithFormat: @"Could not connect to the SMTP server %@", smtpServer];
310      result = [NSException exceptionWithHTTPStatus: 500
311					     reason: @"cannot send message:"
312			    @" (smtp) error when connecting"];
313    }
314  NS_ENDHANDLER;
315
316  return result;
317}
318
319- (NSException *) sendMailData: (NSData *) data
320		  toRecipients: (NSArray *) recipients
321			sender: (NSString *) sender
322             withAuthenticator: (id <SOGoAuthenticator>) authenticator
323                     inContext: (WOContext *) woContext
324{
325  NSException *result;
326
327  if (![recipients count])
328    result = [NSException exceptionWithHTTPStatus: 500
329			  reason: @"cannot send message: no recipients set"];
330  else
331    {
332      if (![sender length])
333	result = [NSException exceptionWithHTTPStatus: 500
334			      reason: @"cannot send message: no sender set"];
335      else
336	{
337	  NSMutableData *cleaned_message;
338	  NSRange r1;
339	  unsigned int limit;
340
341	  //
342	  // We now look for the Bcc: header. If it is present, we remove it.
343	  // Some servers, like qmail, do not remove it automatically.
344	  //
345#warning FIXME - we should fix the case issue when we switch to Pantomime
346	  cleaned_message = [NSMutableData dataWithData: data];
347
348	  // We search only in the headers so we start at 0 until
349	  // we find \r\n\r\n, which is the headers delimiter
350	  r1 = [cleaned_message rangeOfCString: "\r\n\r\n"];
351	  limit = r1.location-1;
352
353	  // We check if the mail actually *starts* with the Bcc: header
354	  r1 = [cleaned_message rangeOfCString: "Bcc: "
355				       options: 0
356					 range: NSMakeRange(0,5)];
357
358	  // It does not, let's search in the entire headers
359	  if (r1.location == NSNotFound)
360	    {
361	      r1 = [cleaned_message rangeOfCString: "\r\nBcc: "
362					   options: 0
363					     range: NSMakeRange(0,limit)];
364	      if (r1.location != NSNotFound)
365		r1.location += 2;
366	    }
367
368	  if (r1.location != NSNotFound)
369	    {
370	      // We search for the first \r\n AFTER the Bcc: header and
371	      // replace the whole thing with \r\n.
372	      unsigned int i;
373
374	      for (i = r1.location+7; i < limit; i++)
375		{
376		  if ([cleaned_message characterAtIndex: i] == '\r' &&
377		      (i+1 < limit && [cleaned_message characterAtIndex: i+1] == '\n') &&
378		      (i+2 < limit && !isspace([cleaned_message characterAtIndex: i+2])))
379		    break;
380		}
381
382	      [cleaned_message replaceBytesInRange: NSMakeRange(r1.location, i-r1.location+2)
383					 withBytes: NULL
384					    length: 0];
385	    }
386
387	  if ([mailingMechanism isEqualToString: @"sendmail"])
388	    result = [self _sendmailSendData: cleaned_message
389			   toRecipients: recipients
390			   sender: [sender pureEMailAddress]];
391	  else
392	    result = [self _smtpSendData: cleaned_message
393                            toRecipients: recipients
394                                  sender: [sender pureEMailAddress]
395                       withAuthenticator: authenticator
396                               inContext: woContext];
397	}
398    }
399
400  return result;
401}
402
403- (NSException *) sendMimePart: (id <NGMimePart>) part
404		  toRecipients: (NSArray *) recipients
405			sender: (NSString *) sender
406             withAuthenticator: (id <SOGoAuthenticator>) authenticator
407                     inContext: (WOContext *) woContext
408{
409  NSData *mailData;
410
411  mailData = [[NGMimePartGenerator mimePartGenerator]
412	       generateMimeFromPart: part];
413
414  return [self sendMailData: mailData
415	       toRecipients: recipients
416                     sender: sender
417          withAuthenticator: authenticator
418                  inContext: woContext];
419}
420
421- (NSException *) sendMailAtPath: (NSString *) filename
422		    toRecipients: (NSArray *) recipients
423			  sender: (NSString *) sender
424               withAuthenticator: (id <SOGoAuthenticator>) authenticator
425                       inContext: (WOContext *) woContext
426{
427  NSException *result;
428  NSData *mailData;
429
430  mailData = [NSData dataWithContentsOfFile: filename];
431  if ([mailData length] > 0)
432    result = [self sendMailData: mailData
433		   toRecipients: recipients
434                         sender: sender
435              withAuthenticator: authenticator
436                      inContext: woContext];
437  else
438    result = [NSException exceptionWithHTTPStatus: 500
439			  reason: @"cannot send message: no data"
440			  @" (missing or empty file?)"];
441
442  return result;
443}
444
445@end
446