TF-106 Fix display inlined image CIDs of emails
This commit is contained in:
@@ -10,6 +10,7 @@ export 'presentation/extensions/html_extension.dart';
|
||||
|
||||
// Exceptions
|
||||
export 'domain/exceptions/download_file_exception.dart';
|
||||
export 'data/extensions/options_extensions.dart';
|
||||
|
||||
// Utils
|
||||
export 'presentation/utils/theme_utils.dart';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
extension OptionsExtension on Options {
|
||||
Options appendHeaders(Map<String, dynamic> additionalHeaders) {
|
||||
if (this.headers != null) {
|
||||
this.headers?.addAll(additionalHeaders);
|
||||
} else {
|
||||
this.headers = additionalHeaders;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:core/core.dart';
|
||||
|
||||
class DioClient {
|
||||
static const jmapHeader = 'application/json;jmapVersion=rfc-8621';
|
||||
@@ -16,10 +19,13 @@ class DioClient {
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
final newOptions = options?.appendHeaders({HttpHeaders.acceptHeader : jmapHeader})
|
||||
?? Options(headers: {HttpHeaders.acceptHeader : jmapHeader}) ;
|
||||
|
||||
return await _dio.get(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
options: newOptions,
|
||||
cancelToken: cancelToken,
|
||||
onReceiveProgress: onReceiveProgress)
|
||||
.then((value) => value.data)
|
||||
@@ -35,10 +41,13 @@ class DioClient {
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
final newOptions = options?.appendHeaders({HttpHeaders.acceptHeader : jmapHeader})
|
||||
?? Options(headers: {HttpHeaders.acceptHeader : jmapHeader}) ;
|
||||
|
||||
return await _dio.post(path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
options: newOptions,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress)
|
||||
@@ -53,11 +62,14 @@ class DioClient {
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
final newOptions = options?.appendHeaders({HttpHeaders.acceptHeader : jmapHeader})
|
||||
?? Options(headers: {HttpHeaders.acceptHeader : jmapHeader}) ;
|
||||
|
||||
return await _dio.delete(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
options: newOptions,
|
||||
cancelToken: cancelToken)
|
||||
.then((value) => value.data)
|
||||
.catchError((error) => throw(error));
|
||||
@@ -72,11 +84,14 @@ class DioClient {
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress
|
||||
}) async {
|
||||
final newOptions = options?.appendHeaders({HttpHeaders.acceptHeader : jmapHeader})
|
||||
?? Options(headers: {HttpHeaders.acceptHeader : jmapHeader}) ;
|
||||
|
||||
return await _dio.put(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
options: newOptions,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
import 'package:html/dom.dart';
|
||||
|
||||
@@ -10,7 +11,13 @@ abstract class DomTransformer {
|
||||
/// Uses the `DOM` [document] and specified [message] to transform the `document`.
|
||||
///
|
||||
/// All changes will be visible to subsequent transformers.
|
||||
void process(Document document, String message, TransformConfiguration configuration);
|
||||
Future<void> process(
|
||||
Document document,
|
||||
String message,
|
||||
Map<String, String>? mapUrlDownloadCID,
|
||||
TransformConfiguration configuration,
|
||||
DioClient dioClient
|
||||
);
|
||||
|
||||
/// Adds a HEAD element if necessary
|
||||
void ensureDocumentHeadIsAvailable(Document document) {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:core/core.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
@@ -8,16 +12,44 @@ class ImageTransformer extends DomTransformer {
|
||||
const ImageTransformer();
|
||||
|
||||
@override
|
||||
void process(Document document, String message, TransformConfiguration configuration) {
|
||||
Future<void> process(
|
||||
Document document,
|
||||
String message,
|
||||
Map<String, String>? mapUrlDownloadCID,
|
||||
TransformConfiguration configuration,
|
||||
DioClient dioClient
|
||||
) async {
|
||||
final imageElements = document.getElementsByTagName('img');
|
||||
for (final imageElement in imageElements) {
|
||||
final src = imageElement.attributes['src'];
|
||||
if (src != null && src.startsWith('http:')) {
|
||||
// always at least enforce HTTPS images:
|
||||
final url = src.substring('http:'.length);
|
||||
imageElement.attributes['src'] = 'https:$url';
|
||||
}
|
||||
|
||||
await Future.wait(imageElements.map((imageElement) async {
|
||||
imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;';
|
||||
final src = imageElement.attributes['src'];
|
||||
if (src != null
|
||||
&& src.isNotEmpty
|
||||
&& src.startsWith('cid:')
|
||||
&& mapUrlDownloadCID != null
|
||||
) {
|
||||
final cid = src.replaceFirst('cid:', '').trim();
|
||||
final cidUrlDownload = mapUrlDownloadCID[cid];
|
||||
if (cidUrlDownload != null && cidUrlDownload.isNotEmpty) {
|
||||
final imgBase64 = await loadAsyncNetworkImageToBase64(dioClient, cidUrlDownload);
|
||||
if (imgBase64 != null && imgBase64.isNotEmpty) {
|
||||
imageElement.attributes['src'] = 'data:image/jpeg;base64,$imgBase64';
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Future<String?> loadAsyncNetworkImageToBase64(DioClient dioClient, String imageUrl) async {
|
||||
try {
|
||||
final response = await dioClient.get(
|
||||
imageUrl,
|
||||
options: Options(responseType: ResponseType.bytes));
|
||||
return base64Encode(response);
|
||||
} catch (e) {
|
||||
return imageUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
import 'package:core/data/network/dio_client.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
import 'package:html/dom.dart';
|
||||
@@ -8,10 +9,16 @@ class EnsureRelationNoReferrerTransformer extends DomTransformer {
|
||||
const EnsureRelationNoReferrerTransformer();
|
||||
|
||||
@override
|
||||
void process(Document document, String message, TransformConfiguration configuration) {
|
||||
Future<void> process(
|
||||
Document document,
|
||||
String message,
|
||||
Map<String, String>? mapUrlDownloadCID,
|
||||
TransformConfiguration configuration,
|
||||
DioClient dioClient
|
||||
) async {
|
||||
final linkElements = document.getElementsByTagName('a');
|
||||
for (final linkElement in linkElements) {
|
||||
await Future.wait(linkElements.map((linkElement) async {
|
||||
linkElement.attributes['rel'] = 'noopener noreferrer';
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
import 'package:core/data/network/dio_client.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
@@ -13,11 +14,18 @@ class ViewPortTransformer extends DomTransformer {
|
||||
const ViewPortTransformer();
|
||||
|
||||
@override
|
||||
void process(Document document, String message, TransformConfiguration configuration) {
|
||||
Future<void> process(
|
||||
Document document,
|
||||
String message,
|
||||
Map<String, String>? mapUrlDownloadCID,
|
||||
TransformConfiguration configuration,
|
||||
DioClient dioClient
|
||||
) async {
|
||||
final metaElements = document.getElementsByTagName('meta');
|
||||
var viewportNeedsToBeAdded = true;
|
||||
var contentTypeNeedsToBeAdded = true;
|
||||
for (final metaElement in metaElements) {
|
||||
|
||||
await Future.wait(metaElements.map((metaElement) async {
|
||||
if (metaElement.attributes['name'] == 'viewport') {
|
||||
viewportNeedsToBeAdded = false;
|
||||
metaElement.attributes['content'] = 'width=device-width, initial-scale=1.0';
|
||||
@@ -30,7 +38,8 @@ class ViewPortTransformer extends DomTransformer {
|
||||
metaElement.attributes['content'] = 'text/html; charset=utf-8';
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
if (contentTypeNeedsToBeAdded) {
|
||||
ensureDocumentHeadIsAvailable(document);
|
||||
document.head!.append(_contentTypeMetaElement);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
|
||||
import 'package:core/data/network/dio_client.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
@@ -8,10 +9,16 @@ class RemoveScriptTransformer extends DomTransformer {
|
||||
const RemoveScriptTransformer();
|
||||
|
||||
@override
|
||||
void process(Document document, String message, TransformConfiguration configuration) {
|
||||
Future<void> process(
|
||||
Document document,
|
||||
String message,
|
||||
Map<String, String>? mapUrlDownloadCID,
|
||||
TransformConfiguration configuration,
|
||||
DioClient dioClient
|
||||
) async {
|
||||
final scriptElements = document.getElementsByTagName('script');
|
||||
for (final scriptElement in scriptElements) {
|
||||
await Future.wait(scriptElements.map((scriptElement) async {
|
||||
scriptElement.remove();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core/core.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/message_content_transformer.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
import 'package:html/dom.dart';
|
||||
@@ -5,21 +6,27 @@ import 'package:html/dom.dart';
|
||||
class HtmlTransform {
|
||||
|
||||
final String _contentHtml;
|
||||
Map<String, String>? mapUrlDownloadCID;
|
||||
DioClient dioClient;
|
||||
|
||||
HtmlTransform(this._contentHtml);
|
||||
HtmlTransform(
|
||||
this._contentHtml,
|
||||
this.dioClient,
|
||||
this.mapUrlDownloadCID
|
||||
);
|
||||
|
||||
/// Transforms this message to HTML code.
|
||||
/// Optionally specify the [transformConfiguration] to control all aspects of the transformation
|
||||
/// - in that case other parameters are ignored.
|
||||
String transformToHtml({TransformConfiguration? transformConfiguration}) {
|
||||
final document = transformToDocument(transformConfiguration: transformConfiguration);
|
||||
Future<String> transformToHtml({TransformConfiguration? transformConfiguration}) async {
|
||||
final document = await transformToDocument(transformConfiguration: transformConfiguration);
|
||||
return document.outerHtml;
|
||||
}
|
||||
|
||||
/// Transforms this message to Document.
|
||||
Document transformToDocument({TransformConfiguration? transformConfiguration}) {
|
||||
Future<Document> transformToDocument({TransformConfiguration? transformConfiguration}) async {
|
||||
transformConfiguration ??= TransformConfiguration.create();
|
||||
final transformer = MessageContentTransformer(transformConfiguration);
|
||||
return transformer.toDocument(_contentHtml);
|
||||
final transformer = MessageContentTransformer(transformConfiguration, dioClient);
|
||||
return await transformer.toDocument(_contentHtml, mapUrlDownloadCID);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:core/core.dart';
|
||||
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
@@ -6,23 +7,24 @@ import 'package:html/parser.dart' show parse;
|
||||
class MessageContentTransformer {
|
||||
/// The configuration used for the transformation
|
||||
final TransformConfiguration configuration;
|
||||
final DioClient dioClient;
|
||||
|
||||
MessageContentTransformer(this.configuration);
|
||||
MessageContentTransformer(this.configuration, this.dioClient);
|
||||
|
||||
void transformDocument(Document document, String message) {
|
||||
for (final domTransformer in configuration.domTransformers) {
|
||||
domTransformer.process(document, message, configuration);
|
||||
}
|
||||
Future<void> transformDocument(
|
||||
Document document,
|
||||
String message,
|
||||
Map<String, String>? mapUrlDownloadCID,
|
||||
) async {
|
||||
await Future.wait(configuration.domTransformers.map((domTransformer) async {
|
||||
await domTransformer.process(document, message, mapUrlDownloadCID, configuration, dioClient);
|
||||
}));
|
||||
}
|
||||
|
||||
Document toDocument(String message) {
|
||||
Future<Document> toDocument(String message, Map<String, String>? mapUrlDownloadCID) async {
|
||||
var html = message;
|
||||
final document = parse(html);
|
||||
transformDocument(document, message);
|
||||
await transformDocument(document, message, mapUrlDownloadCID);
|
||||
return document;
|
||||
}
|
||||
|
||||
String toHtml(String message) {
|
||||
return toDocument(message).outerHtml;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user