Add project

Basic formatting applied. Unnecessary comments have been removed. Suspicious code is covered by TODO.
This commit is contained in:
2025-08-12 11:21:10 +09:00
parent bbcbe841a7
commit 33ab055b43
546 changed files with 176950 additions and 0 deletions

View File

@ -0,0 +1,394 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Hcs.ClientApi.FileStoreServiceApi
{
/// <summary>
/// Описание протокола в файле "ГИС ЖКХ. Альбом ТФФ 14.5.0.1.docx"
/// 2.5 Описание протокола обмена файлами с внешними системами
public class HcsFileStoreServiceApi
{
public HcsClientConfig Config { get; private set; }
/// <summary>
/// Максимальный размер в байтах части файла, которую разрешено
/// загружать на сервер по спецификации протокола
/// </summary>
private const int MAX_PART_LENGTH = 5242880;
public HcsFileStoreServiceApi(HcsClientConfig config)
{
this.Config = config;
}
/// <summary>
/// Путь к сервису хранения файлов ГИС ЖКХ на серевере API
/// </summary>
private const string ExtBusFileStoreServiceRest = "ext-bus-file-store-service/rest";
/// <summary>
/// Получение файла ранее загруженного в ГИС ЖКХ
/// </summary>
public async Task<HcsFile> DownloadFile(
Guid fileGuid, HcsFileStoreContext context, CancellationToken token)
{
long length = await GetFileLength(context, fileGuid, token);
if (length <= MAX_PART_LENGTH) return await DownloadSmallFile(fileGuid, context, token);
return await DownloadLargeFile(fileGuid, length, context, token);
}
/// <summary>
/// Получение файла по частям (не более 5Мб) по GUID файла
/// </summary>
private async Task<HcsFile> DownloadLargeFile(
Guid fileGuid, long fileSize, HcsFileStoreContext context, CancellationToken token)
{
if (fileSize <= MAX_PART_LENGTH)
throw new ArgumentException("Too short file for partial download");
string requestUri = ComposeFileDownloadUri(fileGuid, context);
var resultStream = new MemoryStream();
string resultContentType = null;
using (var client = BuildHttpClient())
{
long doneSize = 0;
while (doneSize < fileSize)
{
long remainderSize = fileSize - doneSize;
long partSize = Math.Min(remainderSize, MAX_PART_LENGTH);
long fromPosition = doneSize;
long toPosition = fromPosition + partSize - 1;
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.Range = new RangeHeaderValue(fromPosition, toPosition);
var response = await SendRequestAsync(client, request, token);
resultContentType = response.Content.Headers.ContentType.ToString();
long? responseSize = response.Content.Headers.ContentLength;
if (responseSize == null || (long)responseSize != partSize)
throw new HcsException($"Получена часть файла длиной {responseSize}, а запрашивалась длина {partSize}");
using (var partStream = await response.Content.ReadAsStreamAsync())
{
partStream.Position = 0;
await partStream.CopyToAsync(resultStream);
}
doneSize += partSize;
}
resultStream.Position = 0;
return new HcsFile(null, resultContentType, resultStream);
}
}
private string ComposeFileDownloadUri(Guid fileGuid, HcsFileStoreContext context)
{
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(fileGuid)}?getfile";
return Config.ComposeEndpointUri(endpointName);
}
/// <summary>
/// Получение файла одной частью (не более 5Мб) по GUID файла
/// </summary>
private async Task<HcsFile> DownloadSmallFile(
Guid fileGuid, HcsFileStoreContext context, CancellationToken token)
{
string requestUri = ComposeFileDownloadUri(fileGuid, context);
using (var client = BuildHttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
var response = await SendRequestAsync(client, request, token);
return new HcsFile(
null,
response.Content.Headers.ContentType.ToString(),
await response.Content.ReadAsStreamAsync());
}
}
public async Task<Guid> UploadFile(HcsFile file, HcsFileStoreContext context, CancellationToken token)
{
if (file is null) throw new ArgumentNullException(nameof(file));
if (file.Length <= MAX_PART_LENGTH)
{
return await UploadSmallFile(file, context, token);
}
else
{
return await UploadLargeFile(file, context, token);
}
}
/// <summary>
/// Отправка большого файла по частям
/// </summary>
private async Task<Guid> UploadLargeFile(HcsFile file, HcsFileStoreContext context, CancellationToken token)
{
using var client = BuildHttpClient();
if (file.Length == 0) throw new ArgumentException("Нельзя передавать файл нулевой длины");
int numParts = (int)Math.Ceiling((double)file.Length / MAX_PART_LENGTH);
Config.Log($"Запрашиваю UploadID для большого файла {file.FileName}");
Guid uploadId = await QueryNewUploadId(client, context, file, numParts, token);
Config.Log($"Получил UploadID {uploadId} для отправки файла {file.FileName} размером {file.Length} байт");
long partOffset = 0;
for (int partNumber = 1; partNumber <= numParts; partNumber++)
{
long lengthLeft = file.Length - partOffset;
int partLength = (int)Math.Min(lengthLeft, MAX_PART_LENGTH);
var partStream = new HcsPartialStream(file.Stream, partOffset, partLength);
Config.Log($"Отправляю часть {partNumber}/{numParts} размером {partLength} байт для файла {file.FileName}");
await UploadFilePart(client, context, uploadId, partNumber, partStream, token);
partOffset += partLength;
}
Config.Log($"Отправляем признак завершения передачи файла {file.FileName}");
await CompleteUpload(client, context, uploadId, token);
Config.Log($"Файл {file.FileName} успешно передан, получен код файла {uploadId}");
return uploadId;
}
/// <summary>
/// Получение кода для загрузки большого файла из нескольких частей менее 5Мб
/// </summary>
private async Task<Guid> QueryNewUploadId(
HttpClient client, HcsFileStoreContext context, HcsFile file, int numParts, CancellationToken token)
{
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/?upload";
string requestUri = Config.ComposeEndpointUri(endpointName);
var content = new StringContent("");
content.Headers.Add("X-Upload-Filename", CleanUploadFileName(file.FileName));
content.Headers.Add("X-Upload-Length", file.Length.ToString());
content.Headers.Add("X-Upload-Part-Count", numParts.ToString());
var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
request.Content = content;
var response = await SendRequestAsync(client, request, token);
return ParseUploadIdFromReponse(response);
}
private async Task UploadFilePart(
HttpClient client, HcsFileStoreContext context, Guid uploadId, int partNumber, Stream partStream,
CancellationToken token)
{
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(uploadId)}";
string requestUri = Config.ComposeEndpointUri(endpointName);
var content = new StreamContent(partStream);
content.Headers.Add("X-Upload-Partnumber", partNumber.ToString());
content.Headers.ContentMD5 = ComputeMD5(partStream);
var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
request.Content = content;
await SendRequestAsync(client, request, token);
}
private async Task CompleteUpload(HttpClient client, HcsFileStoreContext context, Guid uploadId, CancellationToken token)
{
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(uploadId)}?completed";
string requestUri = Config.ComposeEndpointUri(endpointName);
var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
await SendRequestAsync(client, request, token);
}
/// <summary>
/// Загрузка в ГИС ЖКХ файла до 5Мб размером одной операцией
/// </summary>
private async Task<Guid> UploadSmallFile(HcsFile file, HcsFileStoreContext context, CancellationToken token)
{
using var client = BuildHttpClient();
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}";
string requestUri = Config.ComposeEndpointUri(endpointName);
Config.Log($"Начинаю upload малого файла [{file.FileName}] типа [{file.ContentType}] длиной {file.Length}");
if (file.Stream.Length != file.Length)
throw new HcsException($"Длина файла {file.Length} не соответствует размеру данных {file.Stream.Length}");
file.Stream.Position = 0;
var content = new StreamContent(file.Stream);
content.Headers.Add("X-Upload-Filename", CleanUploadFileName(file.FileName));
content.Headers.ContentMD5 = ComputeMD5(file.Stream);
var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
request.Content = content;
var response = await SendRequestAsync(client, request, token);
return ParseUploadIdFromReponse(response);
}
/// <summary>
/// Получение информации о загружаемом или загруженном файле
/// </summary>
public async Task<long> GetFileLength(HcsFileStoreContext context, Guid fileId, CancellationToken token)
{
using var client = BuildHttpClient();
string endpointName = $"{ExtBusFileStoreServiceRest}/{context.GetName()}/{HcsUtil.FormatGuid(fileId)}";
string requestUri = Config.ComposeEndpointUri(endpointName);
var request = new HttpRequestMessage(HttpMethod.Head, requestUri);
var response = await SendRequestAsync(client, request, token);
long length = 0;
var lengthString = SearchResponseHeader(response, "X-Upload-Length");
if (!string.IsNullOrEmpty(lengthString) && long.TryParse(lengthString, out length)) return length;
throw new HcsException("В ответе сервера не указана длина файла");
}
/// <summary>
/// Возвращает вычисленное значение AttachmentHASH для данных файла @stream
/// </summary>
public string ComputeAttachmentHash(Stream stream)
{
var client = Config as HcsClient;
if (client == null) throw new HcsException("Не доступен объект HcsClient для вычиления AttachmentHASH");
// TODO: Проверить комментарий
// В декабре 2024 у меня сломалось вычисление AttachmentHASH для файлов, я стал вычислять
// явно верным алгоритмом ГОСТ94 для больших файлов уже неверное значение суммы. В январе
// 2025 путем перебора вариантов я обнаружил что ГИСЖКХ теперь вычисляет AttachmentHASH
// только по первой части большого файла.
//int hashSourceMaxLength = MAX_PART_LENGTH;
//if (stream.Length <= hashSourceMaxLength) return client.ComputeGost94Hash(stream);
//return client.ComputeGost94Hash(new HcsPartialStream(stream, 0, hashSourceMaxLength));
// 29.01.2025 СТП ГИС ЖКХ ответила что "проведены работы" и теперь
// я вижу что они снова вычисляют AttachmantHASH по полному файлу
return client.ComputeGost94Hash(stream);
}
private async Task<HttpResponseMessage> SendRequestAsync(
HttpClient client, HttpRequestMessage request, CancellationToken token)
{
token.ThrowIfCancellationRequested();
Config.Log($"Отправляю запрос {request.Method} \"{request.RequestUri}\"...");
var response = await client.SendAsync(request, token);
if (response.IsSuccessStatusCode) return response;
throw new HcsException(DescribeResponseError(response));
}
private Guid ParseUploadIdFromReponse(HttpResponseMessage response)
{
string uploadIdheaderName = "X-Upload-UploadID";
var uploadId = SearchResponseHeader(response, uploadIdheaderName);
if (uploadId != null) return HcsUtil.ParseGuid(uploadId);
throw new HcsException($"В ответе сервера нет заголовка {uploadIdheaderName}");
}
private HttpClient BuildHttpClient()
{
var _clientHandler = new HttpClientHandler();
_clientHandler.ClientCertificates.Add(Config.Certificate);
_clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
var client = new HttpClient(_clientHandler);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Add("X-Upload-OrgPPAGUID", Config.OrgPPAGUID);
return client;
}
private string DescribeResponseError(HttpResponseMessage response)
{
string errorHeader = "X-Upload-Error";
var message = SearchResponseHeader(response, errorHeader);
if (message != null)
{
if (knownErrors.ContainsKey(message)) return $"{errorHeader}: {message} ({knownErrors[message]})";
return $"{errorHeader}: {message}";
}
return $"HTTP response status {response.StatusCode}";
}
private string SearchResponseHeader(HttpResponseMessage response, string headerName)
{
if (response.Headers.Any(x => x.Key == headerName))
{
var pair = response.Headers.First(x => x.Key == headerName);
if (pair.Value != null && pair.Value.Any())
{
return pair.Value.First();
}
}
return null;
}
private static Dictionary<string, string> knownErrors = new Dictionary<string, string>() {
{ "FieldValidationException", "не пройдены проверки на корректность заполнения полей (обязательность, формат и т.п.)" },
{ "FileNotFoundException", "не пройдены проверки на существование файла" },
{ "InvalidStatusException", "не пройдены проверки на корректный статус файла" },
{ "InvalidSizeException", "некорректный запрашиваемый размер файла" },
{ "FileVirusInfectionException", "содержимое файла инфицировано" },
{ "FileVirusNotCheckedException", "проверка на вредоносное содержимое не выполнялась" },
{ "FilePermissionException", "организация и внешняя система не имеют полномочий на скачивание файла" },
{ "DataProviderValidationException", "поставщик данных не найден, заблокирован или неактивен" },
{ "CertificateValidationException", "информационная система не найдена по отпечатку или заблокирована" },
{ "HashConflictException", "не пройдены проверки на соответствие контрольной сумме" },
{ "InvalidPartNumberException", "не пройдены проверки на номер части (номер превышает количество частей, указанных в инициализации)" },
{ "ContextNotFoundException", "неверное имя хранилища файлов" },
{ "ExtensionException", "недопустимое расширение файла" },
{ "DetectionException", "не удалось определить MIME-тип загружаемого файла" },
{ "INT002029", "сервис недоступен: выполняются регламентные работы" }
};
private byte[] ComputeMD5(System.IO.Stream stream)
{
var position = stream.Position;
var md5 = System.Security.Cryptography.MD5.Create().ComputeHash(stream);
stream.Position = position;
return md5;
}
/// <summary>
/// Готовит имя размещаемого файла для помещения в заголовок HTTP-запроса
/// </summary>
private string CleanUploadFileName(string fileName)
{
if (fileName is null) return null;
string bannedSymbols = "<>?:|*%\\\"";
var buf = new StringBuilder();
foreach (char ch in fileName)
{
if (bannedSymbols.Contains(ch)) buf.Append('_');
else buf.Append(ch);
}
// Спецификация предписывает кодировать имя файла по стандарту MIME RFC2047
// https://datatracker.ietf.org/doc/html/rfc2047
// Как имя кодировки можно явно использовать константу "windows-1251".
string characterSet = Encoding.Default.WebName;
return EncodedWord.RFC2047.Encode(
buf.ToString(), EncodedWord.RFC2047.ContentEncoding.Base64, characterSet);
}
}
}

View File

@ -0,0 +1,374 @@
//===============================================================================
// RFC2047 (Encoded Word) Decoder
// https://github.com/grumpydev/RFC2047-Encoded-Word-Encoder-Decoder/blob/master/EncodedWord/RFC2047.cs
// http://tools.ietf.org/html/rfc2047
//===============================================================================
// Copyright © Steven Robbins. All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================
namespace EncodedWord
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
/// <summary>
/// Provides support for decoding RFC2047 (Encoded Word) encoded text
/// </summary>
public static class RFC2047
{
/// <summary>
/// Regex for parsing encoded word sections
/// From http://tools.ietf.org/html/rfc2047#section-3
/// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
/// </summary>
private static readonly Regex EncodedWordFormatRegEx = new Regex(@"=\?(?<charset>.*?)\?(?<encoding>[qQbB])\?(?<encodedtext>.*?)\?=", RegexOptions.Singleline | RegexOptions.Compiled);
/// <summary>
/// Regex for removing CRLF SPACE separators from between encoded words
/// </summary>
private static readonly Regex EncodedWordSeparatorRegEx = new Regex(@"\?=\r\n =\?", RegexOptions.Compiled);
/// <summary>
/// Replacement string for removing CRLF SPACE separators
/// </summary>
private const string SeparatorReplacement = @"?==?";
/// <summary>
/// The maximum line length allowed
/// </summary>
private const int MaxLineLength = 75;
/// <summary>
/// Regex for "Q-Encoding" hex bytes from http://tools.ietf.org/html/rfc2047#section-4.2
/// </summary>
private static readonly Regex QEncodingHexCodeRegEx = new Regex(@"(=(?<hexcode>[0-9a-fA-F][0-9a-fA-F]))", RegexOptions.Compiled);
/// <summary>
/// Regex for replacing _ with space as declared in http://tools.ietf.org/html/rfc2047#section-4.2
/// </summary>
private static readonly Regex QEncodingSpaceRegEx = new Regex("_", RegexOptions.Compiled);
/// <summary>
/// Format for an encoded string
/// </summary>
private const string EncodedStringFormat = @"=?{0}?{1}?{2}?=";
/// <summary>
/// Special characters, as defined by RFC2047
/// </summary>
private static readonly char[] SpecialCharacters = { '(', ')', '<', '>', '@', ',', ';', ':', '<', '>', '/', '[', ']', '?', '.', '=', '\t' };
/// <summary>
/// Represents a content encoding type defined in RFC2047
/// </summary>
public enum ContentEncoding
{
/// <summary>
/// Unknown / invalid encoding
/// </summary>
Unknown,
/// <summary>
/// "Q Encoding" (reduced character set) encoding
/// http://tools.ietf.org/html/rfc2047#section-4.2
/// </summary>
QEncoding,
/// <summary>
/// Base 64 encoding
/// http://tools.ietf.org/html/rfc2047#section-4.1
/// </summary>
Base64
}
/// <summary>
/// Encode a string into RFC2047
/// </summary>
/// <param name="plainString">Plain string to encode</param>
/// <param name="contentEncoding">Content encoding to use</param>
/// <param name="characterSet">Character set used by plainString</param>
/// <returns>Encoded string</returns>
public static string Encode(string plainString, ContentEncoding contentEncoding = ContentEncoding.QEncoding, string characterSet = "iso-8859-1")
{
if (String.IsNullOrEmpty(plainString))
{
return String.Empty;
}
if (contentEncoding == ContentEncoding.Unknown)
{
throw new ArgumentException("contentEncoding cannot be unknown for encoding.", "contentEncoding");
}
if (!IsSupportedCharacterSet(characterSet))
{
throw new ArgumentException("characterSet is not supported", "characterSet");
}
var textEncoding = Encoding.GetEncoding(characterSet);
var encoder = GetContentEncoder(contentEncoding);
var encodedContent = encoder.Invoke(plainString, textEncoding);
return BuildEncodedString(characterSet, contentEncoding, encodedContent);
}
/// <summary>
/// Decode a string containing RFC2047 encoded sections
/// </summary>
/// <param name="encodedString">String contaning encoded sections</param>
/// <returns>Decoded string</returns>
public static string Decode(string encodedString)
{
// Remove separators
var decodedString = EncodedWordSeparatorRegEx.Replace(encodedString, SeparatorReplacement);
return EncodedWordFormatRegEx.Replace(
decodedString,
m =>
{
var contentEncoding = GetContentEncodingType(m.Groups["encoding"].Value);
if (contentEncoding == ContentEncoding.Unknown)
{
// Regex should never match, but return anyway
return string.Empty;
}
var characterSet = m.Groups["charset"].Value;
if (!IsSupportedCharacterSet(characterSet))
{
// Fall back to iso-8859-1 if invalid/unsupported character set found
characterSet = @"iso-8859-1";
}
var textEncoding = Encoding.GetEncoding(characterSet);
var contentDecoder = GetContentDecoder(contentEncoding);
var encodedText = m.Groups["encodedtext"].Value;
return contentDecoder.Invoke(encodedText, textEncoding);
});
}
/// <summary>
/// Determines if a character set is supported
/// </summary>
/// <param name="characterSet">Character set name</param>
/// <returns>Bool representing whether the character set is supported</returns>
private static bool IsSupportedCharacterSet(string characterSet)
{
return Encoding.GetEncodings()
.Where(e => String.Equals(e.Name, characterSet, StringComparison.InvariantCultureIgnoreCase))
.Any();
}
/// <summary>
/// Gets the content encoding type from the encoding character
/// </summary>
/// <param name="contentEncodingCharacter">Content contentEncodingCharacter character</param>
/// <returns>ContentEncoding type</returns>
private static ContentEncoding GetContentEncodingType(string contentEncodingCharacter)
{
switch (contentEncodingCharacter)
{
case "Q":
case "q":
return ContentEncoding.QEncoding;
case "B":
case "b":
return ContentEncoding.Base64;
default:
return ContentEncoding.Unknown;
}
}
/// <summary>
/// Gets the content decoder delegate for the given content encoding type
/// </summary>
/// <param name="contentEncoding">Content encoding type</param>
/// <returns>Decoding delegate</returns>
private static Func<string, Encoding, string> GetContentDecoder(ContentEncoding contentEncoding)
{
switch (contentEncoding)
{
case ContentEncoding.Base64:
return DecodeBase64;
case ContentEncoding.QEncoding:
return DecodeQEncoding;
default:
// Will never get here, but return a "null" delegate anyway
return (s, e) => String.Empty;
}
}
/// <summary>
/// Gets the content encoder delegate for the given content encoding type
/// </summary>
/// <param name="contentEncoding">Content encoding type</param>
/// <returns>Encoding delegate</returns>
private static Func<string, Encoding, string> GetContentEncoder(ContentEncoding contentEncoding)
{
switch (contentEncoding)
{
case ContentEncoding.Base64:
return EncodeBase64;
case ContentEncoding.QEncoding:
return EncodeQEncoding;
default:
// Will never get here, but return a "null" delegate anyway
return (s, e) => String.Empty;
}
}
/// <summary>
/// Decodes a base64 encoded string
/// </summary>
/// <param name="encodedText">Encoded text</param>
/// <param name="textEncoder">Encoding instance for the code page required</param>
/// <returns>Decoded string</returns>
private static string DecodeBase64(string encodedText, Encoding textEncoder)
{
var encodedBytes = Convert.FromBase64String(encodedText);
return textEncoder.GetString(encodedBytes);
}
/// <summary>
/// Encodes a base64 encoded string
/// </summary>
/// <param name="plainText">Plain text</param>
/// <param name="textEncoder">Encoding instance for the code page required</param>
/// <returns>Encoded string</returns>
private static string EncodeBase64(string plainText, Encoding textEncoder)
{
var plainTextBytes = textEncoder.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes);
}
/// <summary>
/// Decodes a "Q encoded" string
/// </summary>
/// <param name="encodedText">Encoded text</param>
/// <param name="textEncoder">Encoding instance for the code page required</param>
/// <returns>Decoded string</returns>
private static string DecodeQEncoding(string encodedText, Encoding textEncoder)
{
var decodedText = QEncodingSpaceRegEx.Replace(encodedText, " ");
decodedText = QEncodingHexCodeRegEx.Replace(
decodedText,
m =>
{
var hexString = m.Groups["hexcode"].Value;
int characterValue;
if (!int.TryParse(hexString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out characterValue))
{
return String.Empty;
}
return textEncoder.GetString(new[] { (byte)characterValue });
});
return decodedText;
}
/// <summary>
/// Encodes a "Q encoded" string
/// </summary>
/// <param name="plainText">Plain text</param>
/// <param name="textEncoder">Encoding instance for the code page required</param>
/// <returns>Encoded string</returns>
private static string EncodeQEncoding(string plainText, Encoding textEncoder)
{
if (textEncoder.GetByteCount(plainText) != plainText.Length)
{
throw new ArgumentException("Q encoding only supports single byte encodings", "textEncoder");
}
var specialBytes = textEncoder.GetBytes(SpecialCharacters);
var sb = new StringBuilder(plainText.Length);
var plainBytes = textEncoder.GetBytes(plainText);
// Replace "high" values
for (int i = 0; i < plainBytes.Length; i++)
{
if (plainBytes[i] <= 127 && !specialBytes.Contains(plainBytes[i]))
{
sb.Append(Convert.ToChar(plainBytes[i]));
}
else
{
sb.Append("=");
sb.Append(Convert.ToString(plainBytes[i], 16).ToUpper());
}
}
return sb.ToString().Replace(" ", "_");
}
/// <summary>
/// Builds the full encoded string representation
/// </summary>
/// <param name="characterSet">Characterset to use</param>
/// <param name="contentEncoding">Content encoding to use</param>
/// <param name="encodedContent">Content, encoded to the above parameters</param>
/// <returns>Valid RFC2047 string</returns>
private static string BuildEncodedString(string characterSet, ContentEncoding contentEncoding, string encodedContent)
{
var encodingCharacter = String.Empty;
switch (contentEncoding)
{
case ContentEncoding.Base64:
encodingCharacter = "B";
break;
case ContentEncoding.QEncoding:
encodingCharacter = "Q";
break;
}
var wrapperLength = string.Format(EncodedStringFormat, characterSet, encodingCharacter, String.Empty).Length;
var chunkLength = MaxLineLength - wrapperLength;
if (encodedContent.Length <= chunkLength)
{
return string.Format(EncodedStringFormat, characterSet, encodingCharacter, encodedContent);
}
var sb = new StringBuilder();
foreach (var chunk in SplitStringByLength(encodedContent, chunkLength))
{
sb.AppendFormat(EncodedStringFormat, characterSet, encodingCharacter, chunk);
sb.Append("\r\n ");
}
return sb.ToString();
}
/// <summary>
/// Splits a string into chunks
/// </summary>
/// <param name="inputString">Input string</param>
/// <param name="chunkSize">Size of each chunk</param>
/// <returns>String collection of chunked strings</returns>
public static IEnumerable<string> SplitStringByLength(this string inputString, int chunkSize)
{
for (int index = 0; index < inputString.Length; index += chunkSize)
{
yield return inputString.Substring(index, Math.Min(chunkSize, inputString.Length - index));
}
}
}
}