/* SOGoMailer.m - this file is part of SOGo * * Copyright (C) 2007-2015 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import "NSString+Utilities.h" #import "SOGoStaticAuthenticator.h" #import "SOGoSystemDefaults.h" #import "SOGoUser.h" #import "SOGoUserManager.h" #import "SOGoMailer.h" // // Useful extension that comes from Pantomime which is also // released under the LGPL. We should eventually merge // this with the same category found in SOPE's NGSmtpClient.m // or simply drop sope-mime in favor of Pantomime // @interface NSMutableData (DataCleanupExtension) - (unichar) characterAtIndex: (int) theIndex; - (NSRange) rangeOfCString: (const char *) theCString; - (NSRange) rangeOfCString: (const char *) theCString options: (unsigned int) theOptions range: (NSRange) theRange; @end @implementation NSMutableData (DataCleanupExtension) - (unichar) characterAtIndex: (int) theIndex { const char *bytes; int i, len; len = [self length]; if (len == 0 || theIndex >= len) { [[NSException exceptionWithName: NSRangeException reason: @"Index out of range." userInfo: nil] raise]; return (unichar)0; } bytes = [self bytes]; for (i = 0; i < theIndex; i++) { bytes++; } return (unichar)*bytes; } - (NSRange) rangeOfCString: (const char *) theCString { return [self rangeOfCString: theCString options: 0 range: NSMakeRange(0,[self length])]; } -(NSRange) rangeOfCString: (const char *) theCString options: (unsigned int) theOptions range: (NSRange) theRange { const char *b, *bytes; int i, len, slen; if (!theCString) { return NSMakeRange(NSNotFound,0); } bytes = [self bytes]; len = [self length]; slen = strlen(theCString); b = bytes; if (len > theRange.location + theRange.length) { len = theRange.location + theRange.length; } if (theOptions == NSCaseInsensitiveSearch) { i = theRange.location; b += i; for (; i <= len-slen; i++, b++) { if (!strncasecmp(theCString,b,slen)) { return NSMakeRange(i,slen); } } } else { i = theRange.location; b += i; for (; i <= len-slen; i++, b++) { if (!memcmp(theCString,b,slen)) { return NSMakeRange(i,slen); } } } return NSMakeRange(NSNotFound,0); } @end @implementation SOGoMailer + (SOGoMailer *) mailerWithDomainDefaults: (SOGoDomainDefaults *) dd { return [[self alloc] initWithDomainDefaults: dd]; } - (id) initWithDomainDefaults: (SOGoDomainDefaults *) dd { if ((self = [self init])) { ASSIGN (mailingMechanism, [dd mailingMechanism]); ASSIGN (smtpServer, [dd smtpServer]); ASSIGN (authenticationType, [[dd smtpAuthenticationType] lowercaseString]); } return self; } - (id) init { if ((self = [super init])) { mailingMechanism = nil; smtpServer = nil; authenticationType = nil; } return self; } - (void) dealloc { [mailingMechanism release]; [smtpServer release]; [authenticationType release]; [super dealloc]; } - (BOOL) requiresAuthentication { return ![mailingMechanism isEqualToString: @"sendmail"] && authenticationType; } - (NSException *) _sendmailSendData: (NSData *) mailData toRecipients: (NSArray *) recipients sender: (NSString *) sender { NSException *result; NGSendMail *mailer; mailer = [NGSendMail sharedSendMail]; if ([mailer isSendMailAvailable]) result = [mailer sendMailData: mailData toRecipients: recipients sender: sender]; else result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message:" @" no sendmail binary!"]; return result; } - (NSException *) _sendMailData: (NSData *) mailData withClient: (NGSmtpClient *) client { NSException *result; if ([client sendData: mailData]) result = nil; else result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message:" @" (smtp) failure when sending data"]; return result; } - (NSException *) _smtpSendData: (NSData *) mailData toRecipients: (NSArray *) recipients sender: (NSString *) sender withAuthenticator: (id ) authenticator inContext: (WOContext *) woContext { NSString *currentTo, *login, *password; NSMutableArray *toErrors; NSEnumerator *addresses; NGSmtpClient *client; NSException *result; NSURL * smtpUrl; result = nil; smtpUrl = [[[NSURL alloc] initWithString: smtpServer] autorelease]; client = [NGSmtpClient clientWithURL: smtpUrl]; NS_DURING { [client connect]; if ([authenticationType isEqualToString: @"plain"]) { /* XXX Allow static credentials by peeking at the classname */ if ([authenticator isKindOfClass: [SOGoStaticAuthenticator class]]) login = [(SOGoStaticAuthenticator *)authenticator username]; else login = [[SOGoUserManager sharedUserManager] getExternalLoginForUID: [[authenticator userInContext: woContext] loginInDomain] inDomain: [[authenticator userInContext: woContext] domain]]; password = [authenticator passwordInContext: woContext]; if ([login length] == 0 || [login isEqualToString: @"anonymous"] || ![client plainAuthenticateUser: login withPassword: password]) result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message:" @" (smtp) authentication failure"]; } else if (authenticationType) result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message:" @" unsupported authentication method"]; if (!result) { if ([client mailFrom: sender]) { toErrors = [NSMutableArray array]; addresses = [recipients objectEnumerator]; currentTo = [addresses nextObject]; while (currentTo) { if (![client recipientTo: [currentTo pureEMailAddress]]) { [self logWithFormat: @"error with recipient '%@'", currentTo]; [toErrors addObject: [currentTo pureEMailAddress]]; } currentTo = [addresses nextObject]; } if ([toErrors count] == [recipients count]) result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message:" @" (smtp) all recipients discarded"]; else if ([toErrors count] > 0) result = [NSException exceptionWithHTTPStatus: 500 reason: [NSString stringWithFormat: @"cannot send message (smtp) - recipients discarded:\n%@", [toErrors componentsJoinedByString: @", "]]]; else result = [self _sendMailData: mailData withClient: client]; } else result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message: (smtp) originator not accepted"]; } [client quit]; [client disconnect]; } NS_HANDLER { [self errorWithFormat: @"Could not connect to the SMTP server %@", smtpServer]; if ([localException reason]) { result = [NSException exceptionWithHTTPStatus: 500 reason: [localException reason]]; } else { result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message:" @" (smtp) error when connecting"]; } } NS_ENDHANDLER; return result; } - (NSException *) sendMailData: (NSData *) data toRecipients: (NSArray *) recipients sender: (NSString *) sender withAuthenticator: (id ) authenticator inContext: (WOContext *) woContext { NSException *result; if (![recipients count]) result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message: no recipients set"]; else { if (![sender length]) result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message: no sender set"]; else { NSMutableData *cleaned_message; NSRange r1; unsigned int limit; // // We now look for the Bcc: header. If it is present, we remove it. // Some servers, like qmail, do not remove it automatically. // #warning FIXME - we should fix the case issue when we switch to Pantomime cleaned_message = [NSMutableData dataWithData: data]; // We search only in the headers so we start at 0 until // we find \r\n\r\n, which is the headers delimiter r1 = [cleaned_message rangeOfCString: "\r\n\r\n"]; limit = r1.location-1; // We check if the mail actually *starts* with the Bcc: header r1 = [cleaned_message rangeOfCString: "Bcc: " options: 0 range: NSMakeRange(0,5)]; // It does not, let's search in the entire headers if (r1.location == NSNotFound) { r1 = [cleaned_message rangeOfCString: "\r\nBcc: " options: 0 range: NSMakeRange(0,limit)]; if (r1.location != NSNotFound) r1.location += 2; } if (r1.location != NSNotFound) { // We search for the first \r\n AFTER the Bcc: header and // replace the whole thing with \r\n. unsigned int i; for (i = r1.location+7; i < limit; i++) { if ([cleaned_message characterAtIndex: i] == '\r' && (i+1 < limit && [cleaned_message characterAtIndex: i+1] == '\n') && (i+2 < limit && !isspace([cleaned_message characterAtIndex: i+2]))) break; } [cleaned_message replaceBytesInRange: NSMakeRange(r1.location, i-r1.location+2) withBytes: NULL length: 0]; } if ([mailingMechanism isEqualToString: @"sendmail"]) result = [self _sendmailSendData: cleaned_message toRecipients: recipients sender: [sender pureEMailAddress]]; else result = [self _smtpSendData: cleaned_message toRecipients: recipients sender: [sender pureEMailAddress] withAuthenticator: authenticator inContext: woContext]; } } return result; } - (NSException *) sendMimePart: (id ) part toRecipients: (NSArray *) recipients sender: (NSString *) sender withAuthenticator: (id ) authenticator inContext: (WOContext *) woContext { NSData *mailData; mailData = [[NGMimePartGenerator mimePartGenerator] generateMimeFromPart: part]; return [self sendMailData: mailData toRecipients: recipients sender: sender withAuthenticator: authenticator inContext: woContext]; } - (NSException *) sendMailAtPath: (NSString *) filename toRecipients: (NSArray *) recipients sender: (NSString *) sender withAuthenticator: (id ) authenticator inContext: (WOContext *) woContext { NSException *result; NSData *mailData; mailData = [NSData dataWithContentsOfFile: filename]; if ([mailData length] > 0) result = [self sendMailData: mailData toRecipients: recipients sender: sender withAuthenticator: authenticator inContext: woContext]; else result = [NSException exceptionWithHTTPStatus: 500 reason: @"cannot send message: no data" @" (missing or empty file?)"]; return result; } @end