/*
  Copyright (C) 2017-2021 Inverse inc.

  This file is part of SOGo.

  SOGo is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  SOGo 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 Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with SOGo; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

#if defined(HAVE_OPENSSL) || defined(HAVE_GNUTLS)
#include <openssl/ssl.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/cms.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#endif

#import <Foundation/NSDictionary.h>
#import <Foundation/NSNull.h>
#import <Foundation/NSValue.h>

#import <NGExtensions/NSObject+Logs.h>
#import <NGImap4/NGImap4EnvelopeAddress.h>
#import <NGMime/NGMimeBodyPart.h>
#import <NGMime/NGMimeHeaderFields.h>
#import <NGMime/NGMimeMultipartBody.h>
#import <NGMime/NGMimeType.h>
#import <NGMail/NGMimeMessageParser.h>

#import <SoObjects/Mailer/NSData+SMIME.h>
#import <SoObjects/Mailer/NSString+Mail.h>
#import <SoObjects/Mailer/SOGoMailAccount.h>
#import <SoObjects/Mailer/SOGoMailObject.h>
#import <UI/MailerUI/WOContext+UIxMailer.h>

#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSString+Utilities.h>

#import "UIxMailRenderingContext.h"
#import "UIxMailPartEncryptedViewer.h"

@implementation UIxMailPartEncryptedViewer

#if defined(HAVE_OPENSSL) || defined(HAVE_GNUTLS)
- (X509_STORE *) _setupVerify
{
  X509_STORE *store;
  X509_LOOKUP *lookup;
  BOOL success;

  success = NO;

  store = X509_STORE_new();
  OpenSSL_add_all_algorithms();

  if (store)
    {
      lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
      if (lookup)
        {
          X509_LOOKUP_load_file(lookup, NULL, X509_FILETYPE_DEFAULT);
          lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir());
          if (lookup)
            {
              X509_LOOKUP_add_dir(lookup, NULL, X509_FILETYPE_DEFAULT);
              ERR_clear_error();
              success = YES;
            }
        }
    }

  if (!success)
    {
      if (store)
        {
          X509_STORE_free(store);
          store = NULL;
        }
    }

  return store;
}

- (NSData *) _processMessageWith: (NSData *) signedData
{
  NSData *output;
  NSMutableArray *emails;

  STACK_OF(X509) *certs;
  X509_STORE *x509Store;
  BIO *msgBio, *obio = NULL;
  CMS_ContentInfo *cms;
  int err, i;

  ERR_clear_error();

  msgBio = BIO_new_mem_buf ((void *) [signedData bytes], [signedData length]);
  output = NULL;

  cms = SMIME_read_CMS(msgBio, NULL);

  certs = NULL;
  certificates = [NSMutableArray array];
  emails = [NSMutableArray array];
  validationMessage = nil;

  if (cms)
    {
      if (OBJ_obj2nid(CMS_get0_type(cms)) == NID_pkcs7_signed)
	{
          NSString *subject, *issuer;
	  X509 *x;

	  BIO *dummybio = BIO_new(BIO_s_mem());
	  CMS_verify(cms, NULL, NULL, dummybio, NULL, CMS_NO_SIGNER_CERT_VERIFY | CMS_NO_ATTR_VERIFY | CMS_NO_CONTENT_VERIFY);
	  ERR_clear_error();
	  BIO_free(dummybio);
	  certs = CMS_get0_signers(cms);

          for (i = 0; i < sk_X509_num(certs); i++)
            {
	      BIO *buf;
	      char p[1024];
	      int j;
	      STACK_OF(OPENSSL_STRING) *emlst;

	      x = sk_X509_value(certs, i);

	      emlst = X509_get1_email(x);
	      for (j = 0; j < sk_OPENSSL_STRING_num(emlst); j++)
	          [emails addObject: [[NSString stringWithUTF8String: sk_OPENSSL_STRING_value(emlst, j)] lowercaseString]];
	      X509_email_free(emlst);

	      memset(p, 0, 1024);
	      buf = BIO_new(BIO_s_mem());
	      X509_NAME_print_ex(buf, X509_get_subject_name(x), 0,
                                 ASN1_STRFLGS_ESC_CTRL | ASN1_STRFLGS_UTF8_CONVERT | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN);
              BIO_read(buf, p, 1024);
	      subject = [NSString stringWithUTF8String: p];
	      BIO_free(buf);

	      memset(p, 0, 1024);
	      buf = BIO_new(BIO_s_mem());
	      X509_NAME_print_ex(buf, X509_get_issuer_name(x), 0,
                                 ASN1_STRFLGS_ESC_CTRL | ASN1_STRFLGS_UTF8_CONVERT | XN_FLAG_SEP_MULTILINE | XN_FLAG_FN_LN);
	      BIO_read(buf, p, 1024);
	      issuer = [NSString stringWithUTF8String: p];
	      BIO_free(buf);

              [certificates addObject: [self certificateForSubject: subject
                                                         andIssuer: issuer]];
	    }
	}

      err = ERR_get_error();
      if (err)
	{
	  validSignature = NO;
	}
      else
	{
          x509Store = [self _setupVerify];
          obio = BIO_new(BIO_s_mem());

	  validSignature = (CMS_verify(cms, NULL, x509Store, NULL,
					 obio, 0) == 1);

	  err = ERR_get_error();

	  if (x509Store)
	    X509_STORE_free (x509Store);
	}

      if (err)
        {
#ifdef HAVE_GNUTLS
          const char* sslError;
	  ERR_load_crypto_strings();
          SSL_load_error_strings();
          sslError = ERR_reason_error_string(err);
          validationMessage = [[self labelForKey: sslError ? [NSString stringWithUTF8String: sslError] : @"Digital signature is not valid"] retain];
#elif OPENSSL_VERSION_NUMBER < 0x10100000L
          const char* sslError;
	  ERR_load_crypto_strings();
          SSL_load_error_strings();
          sslError = ERR_reason_error_string(err);
          validationMessage = [[self labelForKey: sslError ? [NSString stringWithUTF8String: sslError] : @"Digital signature is not valid"] retain];
#else
          const char* sslError;
          ERR_load_ERR_strings();
          sslError = ERR_reason_error_string(err);
          validationMessage = [[self labelForKey: sslError ? [NSString stringWithUTF8String: sslError] : @"Digital signature is not valid"] retain];
#endif /* HAVE_GNUTLS */

           BUF_MEM *bptr; //DEL
           BIO_get_mem_ptr(obio, &bptr); //DEL
          // extract contents without validation
          output = [ signedData embeddedContent ];
        }
      else
        {
           BUF_MEM *bptr;
           BIO_get_mem_ptr(obio, &bptr);
           output = [NSData dataWithBytes: bptr->data  length: bptr->length];
        }
    }

  CMS_ContentInfo_free(cms);
  BIO_free (msgBio);
  BIO_free (obio);

  if (validSignature)
    {
      NSString *senderAddress;

      validationMessage = [self labelForKey: @"Message is signed"];
      senderAddress = [[[[[self clientObject] fromEnvelopeAddresses] lastObject] baseEMail] lowercaseString];
      if (![emails containsObject: senderAddress])
        {
          validationMessage = [self labelForKey: @"Message is signed but the certificate doesn't match the sender email address"];
        }
    }
  else if (!validationMessage)
    {
      validationMessage = [NSString stringWithString: [self labelForKey: @"Digital signature is not valid"]];
    }

  processed = YES;
  opaqueSigned = YES;
  return output;
}

- (BOOL) validSignature
{
  if (!processed)
    NSLog(@"ERROR: validSignature called but not processed yet");
    //[self _processMessage];

  return validSignature;
}

- (NSDictionary *) certificateForSubject: (NSString *) subject
                               andIssuer: (NSString *) issuer
{
  return [NSDictionary dictionaryWithObjectsAndKeys:
                              [subject componentsFromMultilineDN], @"subject",
                              [issuer componentsFromMultilineDN], @"issuer",
                       nil];
}

- (NSArray *) smimeCertificates
{
  return certificates;
}

- (NSString *) validationMessage
{
  if (!processed)
    NSLog(@"ERROR: validationMessage called but not processed yet");
    //[self _processMessage];

  return validationMessage;
}
#else
- (NSArray *) smimeCertificates
{
  return nil;
}

- (BOOL) validSignature
{
  return NO;
}

- (NSString *) validationMessage
{
  return nil;
}
#endif

- (void) _attachmentIdsFromBodyPart: (id) thePart
                           partPath: (NSString *) thePartPath
{
  // Small hack to avoid SOPE's stupid behavior to wrap a multipart
  // object in a NGMimeBodyPart.
   if ([thePart isKindOfClass: [NGMimeBodyPart class]] &&
       [[[thePart contentType] type] isEqualToString: @"multipart"])
     thePart = [thePart body];

  if ([thePart isKindOfClass: [NGMimeBodyPart class]])
    {
      NSString *filename, *mimeType;

      mimeType = [[thePart contentType] stringValue];
      filename = [(NGMimeContentDispositionHeaderField *)[thePart headerForKey: @"content-disposition"] filename];

      if (!filename)
        filename = [mimeType asPreferredFilenameUsingPath: nil];

      if (filename)
        {
          [(id)attachmentIds setObject: [NSString stringWithFormat: @"%@%@%@",
                                                  [[self clientObject] baseURLInContext: [self context]],
                                                  thePartPath,
                                                  filename]
                                forKey: [NSString stringWithFormat: @"<%@>", filename]];
        }
    }
  else if ([thePart isKindOfClass: [NGMimeMultipartBody class]])
    {
      int i;

      for (i = 0; i < [[thePart parts] count]; i++)
        {
          [self _attachmentIdsFromBodyPart: [[thePart parts] objectAtIndex: i]
                                  partPath: [NSString stringWithFormat: @"%@%d/", thePartPath, i+1]];
        }
    }
}

- (id) contentViewerComponent
{
  id info;

  info = [self childInfo];
  return [[[self context] mailRenderingContext] viewerForBodyInfo: info];
}

- (id) renderedPart
{
  SOGoMailObject *mailObject;
  NSData *certificate, *decryptedData, *encryptedData;
  id info, viewer;

  mailObject = [[self clientObject] mailObject];
  if ([mailObject isEncrypted])
    {
      encrypted = YES;
      certificate = [[[self clientObject] mailAccountFolder] certificate];
      encryptedData = [[self clientObject] content];
      decryptedData = [encryptedData decryptUsingCertificate: certificate];

      if (decryptedData)
        {
          NGMimeMessageParser *parser;
          NGMimeMessage *message;
          NGMimeType *contentType;
          NSString *type, *subtype, *smimetype;
          id part;

          parser = [[NGMimeMessageParser alloc] init];
          message = [parser parsePartFromData: decryptedData];

          // Extract contents if the encrypted messages contains opaque signed data
          contentType = [message contentType];
          type = [[contentType type] lowercaseString];
          subtype = [[contentType subType] lowercaseString];
          if ([type isEqualToString: @"application"])
            {
              if ([subtype isEqualToString: @"x-pkcs7-mime"] ||
                  [subtype isEqualToString: @"pkcs7-mime"])
                {
                  smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString];
                  if ([smimetype isEqualToString: @"signed-data"])
                    {
                      NGMimeMessageParser *parser;
                      NSData *extractedData;

                      opaqueSigned = YES;
                      extractedData = [self _processMessageWith: decryptedData];
                      if (extractedData)
                        {
                          parser = [[NGMimeMessageParser alloc] init];
                          message = [parser parsePartFromData: extractedData];
                          decryptedData = extractedData;
                          RELEASE(parser);
                        }
                    }
                }
            }

          processed = YES;
	  part = [message retain];

          info = [NSDictionary dictionaryWithObjectsAndKeys: [[part contentType] type], @"type",
                               [[part contentType] subType], @"subtype",
                               [[part contentType] parametersAsDictionary], @"parameterList", nil];
          viewer = [[[self context] mailRenderingContext] viewerForBodyInfo: info];
          [viewer setBodyInfo: info];
          [viewer setFlatContent: decryptedData];
          [viewer setDecodedContent: [part body]];

          // attachmentIds is empty in an ecrypted email as the IMAP body structure
          // is of course not available for file attachments
          [self _attachmentIdsFromBodyPart: [part body]  partPath: @""];
          [viewer setAttachmentIds: attachmentIds];

          return [NSDictionary dictionaryWithObjectsAndKeys:
                                     [self className], @"type",
                                   [NSNumber numberWithBool: YES], @"encrypted",
                                   [NSNumber numberWithBool: YES], @"decrypted",
                                   [NSNumber numberWithBool: opaqueSigned], @"opaqueSigned",
                                   [NSNumber numberWithBool: [self validSignature]], @"valid",
                                   [NSArray arrayWithObject: [viewer renderedPart]], @"content",
                                   [self smimeCertificates], @"certificates",
                                   [self validationMessage], @"message",
                               nil];
        }
    }
  else if ([mailObject isOpaqueSigned])
    {
      NGMimeMessageParser *parser;
      NGMimeMessage *message;
      NSData *extractedData;
      id part;

      opaqueSigned = YES;
      encryptedData = [[self clientObject] content];
      extractedData = [self _processMessageWith: encryptedData];

      if (extractedData)
        {
          parser = [[NGMimeMessageParser alloc] init];
          message = [parser parsePartFromData: extractedData];
          RELEASE(parser);
        }

      processed = YES;
      part = [message retain];

      info = [NSDictionary dictionaryWithObjectsAndKeys: [[part contentType] type], @"type",
                           [[part contentType] subType], @"subtype",
                           [[part contentType] parametersAsDictionary], @"parameterList", nil];
      viewer = [[[self context] mailRenderingContext] viewerForBodyInfo: info];
      [viewer setBodyInfo: info];
      [viewer setFlatContent: extractedData];
      [viewer setDecodedContent: [part body]];

      // attachmentIds is empty in an ecrypted email as the IMAP body structure
      // is of course not available for file attachments
      [self _attachmentIdsFromBodyPart: [part body]  partPath: @""];
      [viewer setAttachmentIds: attachmentIds];

      return [NSDictionary dictionaryWithObjectsAndKeys:
                                 [self className], @"type",
                               [NSNumber numberWithBool: NO], @"encrypted",
                               [NSNumber numberWithBool: YES], @"opaqueSigned",
                               [NSNumber numberWithBool: [self validSignature]], @"valid",
                               [NSArray arrayWithObject: [viewer renderedPart]], @"content",
                               [self smimeCertificates], @"certificates",
                               [self validationMessage], @"message",
                           nil];
    }


  // Decryption failed, let's return something else...
  return [NSDictionary dictionaryWithObjectsAndKeys:
                         [self className], @"type",
                       [NSNumber numberWithBool: encrypted], @"encrypted",
                       [NSNumber numberWithBool: NO], @"decrypted",
                       [NSNumber numberWithBool: NO], @"opaqueSigned",
                       [NSArray array], @"content",
                       nil];
}

@end /* UIxMailPartAlternativeViewer */
