2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "SOGoDraftObject.h"
23 #include <NGMail/NGMimeMessage.h>
24 #include <NGMail/NGMimeMessageGenerator.h>
25 #include <NGMail/NGSendMail.h>
26 #include <NGMime/NGMimeBodyPart.h>
27 #include <NGMime/NGMimeFileData.h>
28 #include <NGMime/NGMimeMultipartBody.h>
29 #include <NGMime/NGMimeType.h>
30 #include <NGImap4/NGImap4Envelope.h>
31 #include <NGImap4/NGImap4EnvelopeAddress.h>
32 #include <NGExtensions/NSFileManager+Extensions.h>
35 @implementation SOGoDraftObject
37 static NGMimeType *TextPlainType = nil;
38 static NGMimeType *MultiMixedType = nil;
39 static NSString *userAgent = @"SOGoMail 1.0";
40 static BOOL draftDeleteDisabled = NO; // for debugging
41 static BOOL debugOn = NO;
44 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
46 if ((draftDeleteDisabled = [ud boolForKey:@"SOGoNoDraftDeleteAfterSend"]))
47 NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)");
49 TextPlainType = [[NGMimeType mimeType:@"text" subType:@"plain"] copy];
50 MultiMixedType = [[NGMimeType mimeType:@"multipart" subType:@"mixed"] copy];
54 [self->envelope release];
60 /* draft folder functionality */
62 - (NSFileManager *)spoolFileManager {
63 return [[self container] spoolFileManager];
65 - (NSString *)userSpoolFolderPath {
66 return [[self container] userSpoolFolderPath];
68 - (BOOL)_ensureUserSpoolFolderPath {
69 return [[self container] _ensureUserSpoolFolderPath];
72 /* draft object functionality */
74 - (NSString *)draftFolderPath {
75 if (self->path != nil)
78 self->path = [[[self userSpoolFolderPath] stringByAppendingPathComponent:
79 [self nameInContainer]] copy];
82 - (BOOL)_ensureDraftFolderPath {
85 if (![self _ensureUserSpoolFolderPath])
88 if ((fm = [self spoolFileManager]) == nil) {
89 [self errorWithFormat:@"missing spool file manager!"];
92 return [fm createDirectoriesAtPath:[self draftFolderPath] attributes:nil];
95 - (NSString *)infoPath {
96 return [[self draftFolderPath]
97 stringByAppendingPathComponent:@".info.plist"];
102 - (NSException *)storeInfo:(NSDictionary *)_info {
104 return [NSException exceptionWithHTTPStatus:500 /* server error */
105 reason:@"got no info to write for draft!"];
107 if (![self _ensureDraftFolderPath]) {
108 [self errorWithFormat:@"could not create folder for draft: '%@'",
109 [self draftFolderPath]];
110 return [NSException exceptionWithHTTPStatus:500 /* server error */
111 reason:@"could not create folder for draft!"];
113 if (![_info writeToFile:[self infoPath] atomically:YES]) {
114 [self errorWithFormat:@"could not write info: '%@'", [self infoPath]];
115 return [NSException exceptionWithHTTPStatus:500 /* server error */
116 reason:@"could not write draft info!"];
119 /* reset info cache */
120 [self->info release]; self->info = nil;
122 return nil /* everything is excellent */;
124 - (NSDictionary *)fetchInfo {
127 if (self->info != nil)
131 if (![[self spoolFileManager] fileExistsAtPath:p]) {
132 [self debugWithFormat:@"Note: info object does not yet exist: %@", p];
136 self->info = [[NSDictionary alloc] initWithContentsOfFile:p];
137 if (self->info == nil)
138 [self errorWithFormat:@"draft info dictionary broken at path: %@", p];
145 - (NSString *)sender {
146 return [[self fetchInfo] objectForKey:@"from"];
151 - (NSArray *)fetchAttachmentNames {
157 fm = [self spoolFileManager];
158 if ((files = [fm directoryContentsAtPath:[self draftFolderPath]]) == nil)
161 count = [files count];
162 ma = [NSMutableArray arrayWithCapacity:count];
163 for (i = 0; i < count; i++) {
166 filename = [files objectAtIndex:i];
167 if ([filename hasPrefix:@"."])
170 [ma addObject:filename];
175 - (BOOL)isValidAttachmentName:(NSString *)_name {
176 static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", @" ", nil };
180 if (![_name isNotNull]) return NO;
181 if ([_name length] == 0) return NO;
182 if ([_name hasPrefix:@"."]) return NO;
184 for (i = 0; sescape[i] != nil; i++) {
185 r = [_name rangeOfString:sescape[i]];
186 if (r.length > 0) return NO;
191 - (NSString *)pathToAttachmentWithName:(NSString *)_name {
192 if ([_name length] == 0)
195 return [[self draftFolderPath] stringByAppendingPathComponent:_name];
198 - (NSException *)invalidAttachmentNameError:(NSString *)_name {
199 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
200 reason:@"Invalid attachment name!"];
203 - (NSException *)saveAttachment:(NSData *)_attach withName:(NSString *)_name {
206 if (![_attach isNotNull]) {
207 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
208 reason:@"Missing attachment content!"];
211 if (![self _ensureDraftFolderPath]) {
212 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
213 reason:@"Could not create folder for draft!"];
215 if (![self isValidAttachmentName:_name])
216 return [self invalidAttachmentNameError:_name];
218 p = [self pathToAttachmentWithName:_name];
219 if (![_attach writeToFile:p atomically:YES]) {
220 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
221 reason:@"Could not write attachment to draft!"];
224 return nil; /* everything OK */
227 - (NSException *)deleteAttachmentWithName:(NSString *)_name {
231 if (![self isValidAttachmentName:_name])
232 return [self invalidAttachmentNameError:_name];
234 fm = [self spoolFileManager];
235 p = [self pathToAttachmentWithName:_name];
236 if (![fm fileExistsAtPath:p])
237 return nil; /* well, doesn't exist, so its deleted ;-) */
239 if (![fm removeFileAtPath:p handler:nil]) {
240 [self logWithFormat:@"ERROR: failed to delete file: %@", p];
241 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
242 reason:@"Could not delete attachment from draft!"];
244 return nil; /* everything OK */
247 /* NGMime representations */
249 - (NGMimeBodyPart *)bodyPartForText {
250 NGMutableHashMap *map;
251 NGMimeBodyPart *bodyPart;
255 if ((lInfo = [self fetchInfo]) == nil)
258 /* prepare header of body part */
260 map = [[[NGMutableHashMap alloc] initWithCapacity:2] autorelease];
262 // TODO: set charset in header!
263 [map setObject:@"text/plain" forKey:@"content-type"];
264 if ((body = [lInfo objectForKey:@"text"]) != nil) {
265 if ([body isKindOfClass:[NSString class]]) {
266 [map setObject:@"text/plain; charset=utf-8" forKey:@"content-type"];
267 body = [body dataUsingEncoding:NSUTF8StringEncoding];
271 /* prepare body content */
273 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
274 [bodyPart setBody:body];
278 - (NGMimeMessage *)mimeMessageForContentWithHeaderMap:(NGMutableHashMap *)map {
280 NGMimeMessage *message;
283 if ((lInfo = [self fetchInfo]) == nil)
286 [map setObject:@"text/plain" forKey:@"content-type"];
287 if ((body = [lInfo objectForKey:@"text"]) != nil) {
288 if ([body isKindOfClass:[NSString class]]) {
289 /* Note: just 'utf8' is displayed wrong in Mail.app */
290 [map setObject:@"text/plain; charset=utf-8" forKey:@"content-type"];
291 body = [body dataUsingEncoding:NSUTF8StringEncoding];
295 message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
296 [message setBody:body];
300 - (NSString *)mimeTypeForExtension:(NSString *)_ext {
301 // TODO: make configurable
302 // TODO: use /etc/mime-types
303 if ([_ext isEqualToString:@"txt"]) return @"text/plain";
304 if ([_ext isEqualToString:@"html"]) return @"text/html";
305 if ([_ext isEqualToString:@"htm"]) return @"text/html";
306 if ([_ext isEqualToString:@"gif"]) return @"image/gif";
307 if ([_ext isEqualToString:@"jpg"]) return @"image/jpeg";
308 if ([_ext isEqualToString:@"jpeg"]) return @"image/jpeg";
309 if ([_ext isEqualToString:@"mail"]) return @"message/rfc822";
310 return @"application/octet-stream";
313 - (NSString *)contentTypeForAttachmentWithName:(NSString *)_name {
316 s = [self mimeTypeForExtension:[_name pathExtension]];
317 if ([_name length] > 0) {
318 s = [s stringByAppendingString:@"; name=\""];
319 s = [s stringByAppendingString:_name];
320 s = [s stringByAppendingString:@"\""];
324 - (NSString *)contentDispositionForAttachmentWithName:(NSString *)_name {
329 type = [self contentTypeForAttachmentWithName:_name];
331 if ([type hasPrefix:@"text/"])
333 else if ([type hasPrefix:@"image/"] || [type hasPrefix:@"message"])
336 cdtype = @"attachment";
338 cd = [cdtype stringByAppendingString:@"; filename=\""];
339 cd = [cd stringByAppendingString:_name];
340 cd = [cd stringByAppendingString:@"\""];
342 // TODO: add size parameter (useful addition, RFC 2183)
346 - (NGMimeBodyPart *)bodyPartForAttachmentWithName:(NSString *)_name {
348 NGMutableHashMap *map;
349 NGMimeBodyPart *bodyPart;
352 BOOL attachAsString, is7bit;
356 if (_name == nil) return nil;
358 /* check attachment */
360 fm = [self spoolFileManager];
361 p = [self pathToAttachmentWithName:_name];
362 if (![fm isReadableFileAtPath:p]) {
363 [self errorWithFormat:@"did not find attachment: '%@'", _name];
369 /* prepare header of body part */
371 map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease];
373 if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) {
374 [map setObject:s forKey:@"content-type"];
375 if ([s hasPrefix:@"text/"])
376 attachAsString = YES;
377 else if ([s hasPrefix:@"message/rfc822"])
380 if ((s = [self contentDispositionForAttachmentWithName:_name]))
381 [map setObject:s forKey:@"content-disposition"];
383 /* prepare body content */
385 if (attachAsString) { // TODO: is this really necessary?
388 content = [[NSData alloc] initWithContentsOfMappedFile:p];
390 s = [[NSString alloc] initWithData:content
391 encoding:[NSString defaultCStringEncoding]];
394 [content release]; content = nil;
397 [self warnWithFormat:
398 @"could not get text attachment as string: '%@'", _name];
405 Note: Apparently NGMimeFileData objects are not processed by the MIME
408 body = [[NGMimeFileData alloc] initWithPath:p removeFile:NO];
409 [map setObject:@"7bit" forKey:@"content-transfer-encoding"];
410 [map setObject:[NSNumber numberWithInt:[body length]]
411 forKey:@"content-length"];
415 Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
416 NGMimeFileData objects are not processed by the MIME generator!
420 content = [[NSData alloc] initWithContentsOfMappedFile:p];
421 encoded = [content dataByEncodingBase64];
422 [content release]; content = nil;
424 [map setObject:@"base64" forKey:@"content-transfer-encoding"];
425 [map setObject:[NSNumber numberWithInt:[encoded length]]
426 forKey:@"content-length"];
428 /* Note: the -init method will create a temporary file! */
429 body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes]
430 length:[encoded length]];
433 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
434 [bodyPart setBody:body];
436 [body release]; body = nil;
440 - (NSArray *)bodyPartsForAllAttachments {
441 /* returns nil on error */
442 NSMutableArray *bodyParts;
446 names = [self fetchAttachmentNames];
447 if ((count = [names count]) == 0)
448 return [NSArray array];
450 bodyParts = [NSMutableArray arrayWithCapacity:count];
451 for (i = 0; i < count; i++) {
452 NGMimeBodyPart *bodyPart;
454 bodyPart = [self bodyPartForAttachmentWithName:[names objectAtIndex:i]];
458 [bodyParts addObject:bodyPart];
463 - (NGMimeMessage *)mimeMultiPartMessageWithHeaderMap:(NGMutableHashMap *)map
464 andBodyParts:(NSArray *)_bodyParts
466 NGMimeMessage *message;
467 NGMimeMultipartBody *mBody;
468 NGMimeBodyPart *part;
471 [map addObject:MultiMixedType forKey:@"content-type"];
473 message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
474 mBody = [[NGMimeMultipartBody alloc] initWithPart:message];
476 part = [self bodyPartForText];
477 [mBody addBodyPart:part];
479 e = [_bodyParts objectEnumerator];
480 while ((part = [e nextObject]) != nil)
481 [mBody addBodyPart:part];
483 [message setBody:mBody];
484 [mBody release]; mBody = nil;
488 - (void)_addHeaders:(NSDictionary *)_h toHeaderMap:(NGMutableHashMap *)_map {
495 names = [_h keyEnumerator];
496 while ((name = [names nextObject]) != nil) {
499 value = [_h objectForKey:name];
500 [_map addObject:value forKey:name];
504 - (NGMutableHashMap *)mimeHeaderMapWithHeaders:(NSDictionary *)_headers {
505 NGMutableHashMap *map;
506 NSDictionary *lInfo; // TODO: this should be some kind of object?
510 if ((lInfo = [self fetchInfo]) == nil)
513 map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
517 if ((emails = [lInfo objectForKey:@"to"]) != nil) {
518 if ([emails count] == 0) {
519 [self errorWithFormat:@"missing 'to' recipient in email!"];
522 [map setObjects:emails forKey:@"to"];
524 if ((emails = [lInfo objectForKey:@"cc"]) != nil)
525 [map setObjects:emails forKey:@"cc"];
526 if ((emails = [lInfo objectForKey:@"bcc"]) != nil)
527 [map setObjects:emails forKey:@"bcc"];
531 if ([(s = [lInfo objectForKey:@"from"]) length] > 0)
532 [map setObject:s forKey:@"from"];
534 if ([(s = [lInfo objectForKey:@"replyTo"]) length] > 0)
535 [map setObject:s forKey:@"reply-to"];
536 else if ([(s = [lInfo objectForKey:@"from"]) length] > 0)
537 [map setObject:s forKey:@"reply-to"];
541 if ([(s = [lInfo objectForKey:@"subject"]) length] > 0)
542 [map setObject:s forKey:@"subject"];
544 /* add standard headers */
546 [map addObject:[NSCalendarDate date] forKey:@"date"];
547 [map addObject:@"1.0" forKey:@"MIME-Version"];
548 [map addObject:userAgent forKey:@"X-Mailer"];
550 /* add custom headers */
552 [self _addHeaders:[lInfo objectForKey:@"headers"] toHeaderMap:map];
553 [self _addHeaders:_headers toHeaderMap:map];
558 - (NGMimeMessage *)mimeMessageWithHeaders:(NSDictionary *)_headers {
559 NSAutoreleasePool *pool;
560 NGMutableHashMap *map;
562 NGMimeMessage *message;
564 pool = [[NSAutoreleasePool alloc] init];
566 if ([self fetchInfo] == nil) {
567 [self errorWithFormat:@"could not locate draft fetch info!"];
571 if ((map = [self mimeHeaderMapWithHeaders:_headers]) == nil)
573 [self debugWithFormat:@"MIME Envelope: %@", map];
575 if ((bodyParts = [self bodyPartsForAllAttachments]) == nil) {
576 [self errorWithFormat:
577 @"could not create body parts for attachments!"];
578 return nil; // TODO: improve error handling, return exception
580 [self debugWithFormat:@"attachments: %@", bodyParts];
582 if ([bodyParts count] == 0) {
584 message = [self mimeMessageForContentWithHeaderMap:map];
587 /* attachments, create multipart/mixed */
588 message = [self mimeMultiPartMessageWithHeaderMap:map
589 andBodyParts:bodyParts];
591 [self debugWithFormat:@"message: %@", message];
593 message = [message retain];
595 return [message autorelease];
597 - (NGMimeMessage *)mimeMessage {
598 return [self mimeMessageWithHeaders:nil];
601 - (NSString *)saveMimeMessageToTemporaryFileWithHeaders:(NSDictionary *)_h {
602 NGMimeMessageGenerator *gen;
603 NSAutoreleasePool *pool;
604 NGMimeMessage *message;
607 pool = [[NSAutoreleasePool alloc] init];
609 message = [self mimeMessageWithHeaders:_h];
610 if (![message isNotNull])
612 if ([message isKindOfClass:[NSException class]]) {
613 [self errorWithFormat:@"error: %@", message];
617 gen = [[NGMimeMessageGenerator alloc] init];
618 tmpPath = [[gen generateMimeFromPartToFile:message] copy];
619 [gen release]; gen = nil;
622 return [tmpPath autorelease];
624 - (NSString *)saveMimeMessageToTemporaryFile {
625 return [self saveMimeMessageToTemporaryFileWithHeaders:nil];
628 - (void)deleteTemporaryMessageFile:(NSString *)_path {
631 if (![_path isNotNull])
634 fm = [NSFileManager defaultManager];
635 if (![fm fileExistsAtPath:_path])
638 [fm removeFileAtPath:_path handler:nil];
641 - (NSArray *)allRecipients {
646 if ((lInfo = [self fetchInfo]) == nil)
649 ma = [NSMutableArray arrayWithCapacity:16];
650 if ((tmp = [lInfo objectForKey:@"to"]) != nil)
651 [ma addObjectsFromArray:tmp];
652 if ((tmp = [lInfo objectForKey:@"cc"]) != nil)
653 [ma addObjectsFromArray:tmp];
654 if ((tmp = [lInfo objectForKey:@"bcc"]) != nil)
655 [ma addObjectsFromArray:tmp];
659 - (NSException *)sendMimeMessageAtPath:(NSString *)_path {
660 static NGSendMail *mailer = nil;
666 recipients = [self allRecipients];
667 from = [self sender];
668 if ([recipients count] == 0) {
669 return [NSException exceptionWithHTTPStatus:500 /* server error */
670 reason:@"draft has no recipients set!"];
672 if ([from length] == 0) {
673 return [NSException exceptionWithHTTPStatus:500 /* server error */
674 reason:@"draft has no sender (from) set!"];
677 /* setup mailer object */
680 mailer = [[NGSendMail sharedSendMail] retain];
681 if (![mailer isSendMailAvailable]) {
682 [self errorWithFormat:@"missing sendmail binary!"];
683 return [NSException exceptionWithHTTPStatus:500 /* server error */
684 reason:@"did not find sendmail binary!"];
689 return [mailer sendMailAtPath:_path toRecipients:recipients sender:from];
692 - (NSException *)sendMail {
696 /* save MIME mail to file */
698 tmpPath = [self saveMimeMessageToTemporaryFile];
699 if (![tmpPath isNotNull]) {
700 return [NSException exceptionWithHTTPStatus:500 /* server error */
701 reason:@"could not save MIME message for draft!"];
705 error = [self sendMimeMessageAtPath:tmpPath];
707 /* delete temporary file */
708 [self deleteTemporaryMessageFile:tmpPath];
715 - (NSException *)delete {
720 if ((fm = [self spoolFileManager]) == nil) {
721 [self errorWithFormat:@"missing spool file manager!"];
722 return [NSException exceptionWithHTTPStatus:500 /* server error */
723 reason:@"missing spool file manager!"];
726 p = [self draftFolderPath];
727 if (![fm fileExistsAtPath:p]) {
728 return [NSException exceptionWithHTTPStatus:404 /* not found */
729 reason:@"did not find draft!"];
732 e = [[fm directoryContentsAtPath:p] objectEnumerator];
733 while ((sp = [e nextObject])) {
734 sp = [p stringByAppendingPathComponent:sp];
735 if (draftDeleteDisabled) {
736 [self logWithFormat:@"should delete draft file %@ ...", sp];
740 if (![fm removeFileAtPath:sp handler:nil]) {
741 return [NSException exceptionWithHTTPStatus:500 /* server error */
742 reason:@"failed to delete draft!"];
746 if (draftDeleteDisabled) {
747 [self logWithFormat:@"should delete draft directory: %@", p];
750 if (![fm removeFileAtPath:p handler:nil]) {
751 return [NSException exceptionWithHTTPStatus:500 /* server error */
752 reason:@"failed to delete draft directory!"];
758 - (NSData *)content {
759 /* Note: does not cache, expensive operation */
763 if ((p = [self saveMimeMessageToTemporaryFile]) == nil)
766 data = [NSData dataWithContentsOfMappedFile:p];
768 /* delete temporary file */
769 [self deleteTemporaryMessageFile:p];
773 - (NSString *)contentAsString {
777 if ((data = [self content]) == nil)
780 str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
782 [self errorWithFormat:@"could not load draft as ASCII (data size=%d)",
787 return [str autorelease];
792 - (id)DELETEAction:(id)_ctx {
795 if ((error = [self delete]) != nil)
798 return [NSNumber numberWithBool:YES]; /* delete worked out ... */
801 - (id)GETAction:(WOContext *)_ctx {
803 Override, because SOGoObject's GETAction uses the less efficient
804 -contentAsString method.
809 if ([rq isSoWebDAVRequest]) {
813 if ((content = [self content]) == nil) {
814 return [NSException exceptionWithHTTPStatus:500
815 reason:@"Could not generate MIME content!"];
818 [r setHeader:@"message/rfc822" forKey:@"content-type"];
819 [r setContent:content];
823 return [super GETAction:_ctx];
826 /* fake being a SOGoMailObject */
828 - (id)fetchParts:(NSArray *)_parts {
829 return [NSDictionary dictionaryWithObject:self forKey:@"fetch"];
833 return [self nameInContainer];
836 static NSArray *seenFlags = nil;
837 seenFlags = [[NSArray alloc] initWithObjects:@"seen", nil];
841 // TODO: size, hard to support, we would need to generate MIME?
845 - (NSArray *)imap4EnvelopeAddressesForStrings:(NSArray *)_emails {
851 if ((count = [_emails count]) == 0)
852 return [NSArray array];
854 ma = [NSMutableArray arrayWithCapacity:count];
855 for (i = 0; i < count; i++) {
856 NGImap4EnvelopeAddress *envaddr;
858 envaddr = [[NGImap4EnvelopeAddress alloc]
859 initWithString:[_emails objectAtIndex:i]];
860 if ([envaddr isNotNull])
861 [ma addObject:envaddr];
867 - (NGImap4Envelope *)envelope {
870 if (self->envelope != nil)
871 return self->envelope;
872 if ((lInfo = [self fetchInfo]) == nil)
876 [[NGImap4Envelope alloc] initWithMessageID:[self nameInContainer]
877 subject:[lInfo objectForKey:@"subject"]
879 replyTo:[lInfo objectForKey:@"replyTo"]
880 to:[lInfo objectForKey:@"to"]
881 cc:[lInfo objectForKey:@"cc"]
882 bcc:[lInfo objectForKey:@"bcc"]];
883 return self->envelope;
888 - (BOOL)isDebuggingEnabled {
892 @end /* SOGoDraftObject */