text: ﻿using System;
  using System.Collections.Generic;
  using System.Text;
  using System.Text.RegularExpressions;
  
  namespace LumiSoft.Net.MIME
  {
      /// <summary>
      /// Implements 'encoded-word' encoding. Defined in RFC 2047.
      /// </summary>
      public class MIME_Encoding_EncodedWord
      {
          private MIME_EncodedWordEncoding m_Encoding;
          private Encoding                 m_pCharset = null;
          private bool                     m_Split    = true;
  
          //private static readonly Regex encodedword_regex = new Regex(@"=\?(((?<charset>.*?)\*.*?)|(?<charset>.*?))\?(?<encoding>[qQbB])\?(?<value>.*?)\?=(?<whitespaces>\s*)",RegexOptions.IgnoreCase);
          private static readonly Regex encodedword_regex = new Regex(@"\=\?(?<charset>\S+?)\?(?<encoding>[qQbB])\?(?<value>.+?)\?\=(?<whitespaces>\s*)",RegexOptions.IgnoreCase);
  
          /// <summary>
          /// Default constructor.
          /// </summary>
          /// <param name="encoding">Encoding to use to encode text.</param>
          /// <param name="charset">Charset to use for encoding. If not sure UTF-8 is strongly recommended.</param>
          /// <exception cref="ArgumentNullException">Is raised when <b>charset</b> is null reference.</exception>
          public MIME_Encoding_EncodedWord(MIME_EncodedWordEncoding encoding,Encoding charset)
          {
              if(charset == null){
                  throw new ArgumentNullException("charset");
              }
  
              m_Encoding = encoding;
              m_pCharset = charset;
          }
  
                  
          #region method Encode
  
          /// <summary>
          /// Encodes specified text if it contains 8-bit chars, otherwise text won't be encoded.
          /// </summary>
          /// <param name="text">Text to encode.</param>
          /// <returns>Returns encoded text.</returns>
          public string Encode(string text)
          {            
              if(MustEncode(text)){
                  return EncodeS(m_Encoding,m_pCharset,m_Split,text);
              }
              else{
                  return text;
              }
          }
  
          #endregion
  
          #region method Decode
  
          /// <summary>
          /// Decodes specified encoded-word.
          /// </summary>
          /// <param name="text">Encoded-word value.</param>
          /// <returns>Returns decoded text.</returns>
          /// <exception cref="ArgumentNullException">Is raised when <b>text</b> is null reference.</exception>
          public string Decode(string text)
          {
              if(text == null){
                  throw new ArgumentNullException("text");
              }
  
              return DecodeS(text);
          }
  
          #endregion
  
  
          #region static method MustEncode
  
          /// <summary>
          /// Checks if specified text must be encoded.
          /// </summary>
          /// <param name="text">Text to encode.</param>
          /// <returns>Returns true if specified text must be encoded, otherwise false.</returns>
          /// <exception cref="ArgumentNullException">Is raised when <b>text</b> is null reference.</exception>
          public static bool MustEncode(string text)
          {
              if(text == null){
                  throw new ArgumentNullException("text");
              }
  
              // Encoding is needed only for non-ASCII chars.
  
              foreach(char c in text){
                  if(c > 127){
                      return true;
                  }
              }
  
              return false;
          }
  
          #endregion
  
          #region static method EncodeS
  
          /// <summary>
          /// Encodes specified text if it contains 8-bit chars, otherwise text won't be encoded.
          /// </summary>
          /// <param name="encoding">Encoding to use to encode text.</param>
          /// <param name="charset">Charset to use for encoding. If not sure UTF-8 is strongly recommended.</param>
          /// <param name="split">If true, words are splitted after 75 chars.</param>
          /// <param name="text">Text to encode.</param>
          /// <returns>Returns encoded text.</returns>
          /// <exception cref="ArgumentNullException">Is raised when <b>charset</b> or <b>text</b> is null reference.</exception>
          public static string EncodeS(MIME_EncodedWordEncoding encoding,Encoding charset,bool split,string text)
          {
              if(charset == null){
                  throw new ArgumentNullException("charset");
              }
              if(text == null){
                  throw new ArgumentNullException("text");
              }
  
              /* RFC 2047 2.
                  encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
               
                  An 'encoded-word' may not be more than 75 characters long, including
                  'charset', 'encoding', 'encoded-text', and delimiters.  If it is
                  desirable to encode more text than will fit in an 'encoded-word' of
                  75 characters, multiple 'encoded-word's (separated by CRLF SPACE) may
                  be used.
               
                 RFC 2231 (updates syntax)
                  encoded-word := "=?" charset ["*" language] "?" encoded-text "?="
              */
  
              if(MustEncode(text)){
                  List<string> parts = new List<string>();
                  if(split){
                      int index = 0;
                      // We just split text to 30 char words, then if some chars encoded, we don't exceed 75 chars lenght limit.
                      while(index < text.Length){
                          int countReaded = Math.Min(30,text.Length - index);
                          parts.Add(text.Substring(index,countReaded));                        
                          index += countReaded;                        
                      }
                  }
                  else{
                      parts.Add(text);
                  }
  
                  StringBuilder retVal = new StringBuilder();
                  for(int i=0;i<parts.Count;i++){
                      string part = parts[i];
                      byte[] data = charset.GetBytes(part);
  
                      #region B encode
  
                      if(encoding == MIME_EncodedWordEncoding.B){
                          retVal.Append("=?" + charset.WebName + "?B?" + Convert.ToBase64String(data) + "?=");
                      }
  
                      #endregion
  
                      #region Q encode
  
                      else{
                          retVal.Append("=?" + charset.WebName + "?Q?");
                          int stored = 0;
                          foreach(byte b in data){
                              string val = null;
                              // We need to encode byte. Defined in RFC 2047 4.2.
                              if(b > 127 || b == '=' || b == '?' || b == '_' || b == ' '){
                                  val = "=" + b.ToString("X2");
                              }
                              else{
                                  val = ((char)b).ToString();
                              }
  
                              retVal.Append(val);
                              stored += val.Length;
                          }
                          retVal.Append("?=");
                      }
  
                      #endregion
  
                      if(i < (parts.Count - 1)){
                          retVal.Append("\r\n ");
                      }
                  }
  
                  return retVal.ToString();
              }
              else{
                  return text;
              }
          }
  
          #endregion
  
          #region static method DecodeS
  
          /// <summary>
          /// Decodes non-ascii word with MIME <b>encoded-word</b> method. Defined in RFC 2047 2.
          /// </summary>
          /// <param name="word">MIME encoded-word value.</param>
          /// <returns>Returns decoded word.</returns>
          /// <remarks>If <b>word</b> is not encoded-word or has invalid syntax, <b>word</b> is leaved as is.</remarks>
          /// <exception cref="ArgumentNullException">Is raised when <b>word</b> is null reference.</exception>
          public static string DecodeS(string word)
          {
              if(word == null){
                  throw new ArgumentNullException("word");
              }
  
              return DecodeTextS(word);
          }
  
          #endregion
  
          #region static method DecodeTextS
  
          /// <summary>
          /// Decodes non-ascii text with MIME <b>encoded-word</b> method. Defined in RFC 2047 2.
          /// </summary>
          /// <param name="text">Text.</param>
          /// <returns>Returns decoded text.</returns>
          /// <exception cref="ArgumentNullException">Is raised when <b>text</b> is null reference.</exception>
          public static string DecodeTextS(string text)
          {
              if(text == null){
                  throw new ArgumentNullException("word");
              }
  
              /* RFC 2047 2.
                  encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
               
                  encoded-text = 1*<Any printable ASCII character other than "?" or SPACE>
                                 ; (but see "Use of encoded-words in message
                                 ; headers", section 5)
              
                  An 'encoded-word' may not be more than 75 characters long, including
                  'charset', 'encoding', 'encoded-text', and delimiters.  If it is
                  desirable to encode more text than will fit in an 'encoded-word' of
                  75 characters, multiple 'encoded-word's (separated by CRLF SPACE) may
                  be used.
               
                  RFC 2231 updates.
                      encoded-word := "=?" charset ["*" language] "?" encoded-text "?="
              */
  
              string retVal = text;
  
              retVal = encodedword_regex.Replace(retVal,delegate(Match m){
                  // We have encoded word, try to decode it.
                  // Also if we have continuing encoded word, we need to skip all whitespaces between words.
               
                  string encodedWord = m.Value;
                  try{
                      if(string.Equals(m.Groups["encoding"].Value,"Q",StringComparison.InvariantCultureIgnoreCase)){
                          encodedWord =  MIME_Utils.QDecode(Encoding.GetEncoding(m.Groups["charset"].Value),m.Groups["value"].Value);
                      }
                      else if(string.Equals(m.Groups["encoding"].Value,"B",StringComparison.InvariantCultureIgnoreCase)){
                          encodedWord = Encoding.GetEncoding(m.Groups["charset"].Value).GetString(Net_Utils.FromBase64(Encoding.Default.GetBytes(m.Groups["value"].Value)));
                      }
                      // Failed to parse encoded-word, leave it as is. RFC 2047 6.3.
                      // else{
  
                      // No continuing encoded-word, append whitespaces to retval.
                      Match mNext = encodedword_regex.Match(retVal,m.Index + m.Length);
                      if(!(mNext.Success && mNext.Index == (m.Index + m.Length))){
                          encodedWord += m.Groups["whitespaces"].Value;
                      }
                      // We have continuing encoded-word, so skip all whitespaces.
                      //else{
  
                      return encodedWord;
                  }
                  catch{
                      // Failed to parse encoded-word, leave it as is. RFC 2047 6.3.
                      return encodedWord;
                  }
              });   
  
              return retVal;
          }
  
          #endregion
  
  
          #region Properties implementation
  
          /// <summary>
          /// Gets or sets if long words(over 75 char) are splitted.
          /// </summary>
          public bool Split
          {
              get{ return m_Split; }
  
              set{ m_Split = value; }
          }
  
          #endregion
  
      }
  }
  