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