Skip to content

Commit 83f00aa

Browse files
rnwoodweb-flow
andauthored
feat: part preview for images and PDFs (#1877)
* feat: Part preview for images and PDFs * Slight layout tweak and fix tests. --------- Co-authored-by: smtp4dev-automation <[email protected]>
1 parent 9850dd5 commit 83f00aa

File tree

7 files changed

+82
-26
lines changed

7 files changed

+82
-26
lines changed

Rnwood.Smtp4dev.Tests/E2E/PageModel/MessageView.cs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,9 @@ public MessageView(IPage page)
1313
}
1414

1515
public async Task<ILocator> GetViewTabAsync()
16-
{
17-
// Try primary selector first
18-
try
19-
{
20-
var tab = page.Locator("div[contains(@class, 'el-tab-pane')][id='view']//ancestor::div[contains(@class, 'el-tabs')]//div[contains(@class, 'el-tabs__item')]:has-text('View')");
21-
await tab.WaitForAsync(new LocatorWaitForOptions { Timeout = 1000 });
22-
return tab;
23-
}
24-
catch
25-
{
26-
// Fallback selector
27-
return page.Locator("div[class*='el-tabs__item']:has-text('View')");
28-
}
16+
{
17+
// Fallback selector
18+
return page.Locator("div[class*='el-tabs__item']:text-is('View')");
2919
}
3020

3121
public async Task<ILocator> GetHtmlSubTabAsync()
@@ -89,7 +79,7 @@ public async Task<string> GetHtmlFrameContentAsync()
8979
{
9080
var frame = await GetHtmlFrameAsync();
9181
var frameHandle = await frame.ElementHandleAsync();
92-
82+
9383
if (frameHandle != null)
9484
{
9585
var frameContent = await frameHandle.ContentFrameAsync();
@@ -99,7 +89,7 @@ public async Task<string> GetHtmlFrameContentAsync()
9989
return await body.InnerHTMLAsync();
10090
}
10191
}
102-
92+
10393
return "";
10494
}
10595

Rnwood.Smtp4dev/ApiModel/Message.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,24 @@ private MessageEntitySummary HandleMimeEntity(MimeEntity entity)
166166

167167
}
168168

169-
internal static FileStreamResult GetPartContent(Message result, string cid)
169+
internal static FileStreamResult GetPartContent(Message result, string cid, bool download= false)
170170
{
171171
var contentEntity = GetPart(result, cid);
172172

173173
if (contentEntity is MimePart mimePart && mimePart.Content != null)
174174
{
175+
string fileDownloadName = null;
176+
177+
if (download)
178+
{
179+
fileDownloadName = mimePart.FileName ??
180+
((contentEntity.ContentId ?? "content") + (MimeTypes.TryGetExtension(mimePart.ContentType.MimeType, out string extn) ? extn : ""));
181+
}
182+
183+
175184
return new FileStreamResult(mimePart.Content.Open(), contentEntity.ContentType?.MimeType ?? "application/text")
176185
{
177-
FileDownloadName = mimePart.FileName ??
178-
((contentEntity.ContentId ?? "content") + (MimeTypes.TryGetExtension(mimePart.ContentType.MimeType, out string extn) ? extn : ""))
186+
FileDownloadName = fileDownloadName
179187
};
180188
}
181189
else

Rnwood.Smtp4dev/ClientApp/src/ApiClient/MessagesController.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,13 @@ export default class MessagesController {
9696
}
9797

9898
// get: api/Messages/${encodeURIComponent(id)}/part/${encodeURIComponent(partid)}/content
99-
public getPartContent_url(id: string, partid: string): string {
100-
return `${this.apiBaseUrl}/${encodeURIComponent(id)}/part/${encodeURIComponent(partid)}/content`;
99+
public getPartContent_url(id: string, partid: string, download: boolean): string {
100+
return `${this.apiBaseUrl}/${encodeURIComponent(id)}/part/${encodeURIComponent(partid)}/content?download=${download}`;
101101
}
102102

103-
public async getPartContent(id: string, partid: string): Promise<FileStreamResult> {
103+
public async getPartContent(id: string, partid: string, download: boolean): Promise<FileStreamResult> {
104104

105-
return (await axios.get(this.getPartContent_url(id, partid), null || undefined)).data as FileStreamResult;
105+
return (await axios.get(this.getPartContent_url(id, partid, download), null || undefined)).data as FileStreamResult;
106106
}
107107

108108
// get: api/Messages/${encodeURIComponent(id)}/part/${encodeURIComponent(partid)}/source
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<template>
2+
<div v-if="isPreviewable && contentUrl" class="messagepartpreview hfillpanel">
3+
<iframe :src="contentUrl" frameborder="0" class="fill" />
4+
</div>
5+
<div v-else class="fill nodetails centrecontents">
6+
<div>Not previewable.</div>
7+
</div>
8+
</template>
9+
10+
<script lang="ts">
11+
import { Component, Prop, Vue, toNative } from 'vue-facing-decorator';
12+
import MessageEntitySummary from '../ApiClient/MessageEntitySummary';
13+
import Message from '../ApiClient/Message';
14+
import MessagesController from '../ApiClient/MessagesController';
15+
16+
@Component({})
17+
class MessagePartPreview extends Vue {
18+
@Prop({ required: true })
19+
message!: Message;
20+
21+
@Prop({ required: true })
22+
part!: MessageEntitySummary;
23+
24+
get contentType(): string {
25+
const header = this.part.headers.find(h => h.name.toLowerCase() === 'content-type');
26+
return header ? header.value.split(';')[0].trim().toLowerCase() : '';
27+
}
28+
29+
get isPreviewable(): boolean {
30+
// Add more types as needed (e.g., text/html, text/plain, application/pdf)
31+
return this.contentType.startsWith('image/') || this.contentType === 'application/pdf';
32+
}
33+
34+
get contentUrl(): string | null {
35+
if (!this.isPreviewable) return null;
36+
return new MessagesController().getPartContent_url(this.message.id, this.part.id, false);
37+
}
38+
}
39+
40+
export default toNative(MessagePartPreview);
41+
</script>

Rnwood.Smtp4dev/ClientApp/src/components/messagepartsource.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
} else {
8181
8282
this.sourceurl = new MessagesController().getPartSource_url(this.messageEntitySummary.messageId, this.messageEntitySummary.id);
83-
this.downloadurl = new MessagesController().getPartContent_url(this.messageEntitySummary.messageId, this.messageEntitySummary.id);
83+
this.downloadurl = new MessagesController().getPartContent_url(this.messageEntitySummary.messageId, this.messageEntitySummary.id, true);
8484
this.source = await new MessagesController().getPartSource(this.messageEntitySummary.messageId, this.messageEntitySummary.id);
8585
this.sourceLang = this.messageEntitySummary.headers.find(h => h.name.toLowerCase() == "content-type" && h.value.indexOf("text/html") === 0) ? "html" : "text";
8686

Rnwood.Smtp4dev/ClientApp/src/components/messageview.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,14 @@
186186
</el-tree>
187187

188188
<div v-show="selectedPart" class="fill vfillpanel">
189-
<el-tabs value="headers" class="fill hfillpanel" type="border-card">
189+
<el-tabs v-if="selectedPart" value="preview" class="fill hfillpanel" type="border-card">
190+
191+
<el-tab-pane label="Preview" id="preview" class="fill vfillpanel">
192+
<template #label>
193+
<el-icon><View /></el-icon>&nbsp;Preview
194+
</template>
195+
<messagepartpreview :message="message" :part="selectedPart" class="nopad fill" />
196+
</el-tab-pane>
190197
<el-tab-pane label="Headers" id="headers" class="fill vfillpanel">
191198
<template #label>
192199
<el-icon><Memo /></el-icon>&nbsp;Headers
@@ -207,6 +214,7 @@
207214
</template>
208215
<messagepartsource class="fill" :messageEntitySummary="selectedPart" type="raw"></messagepartsource>
209216
</el-tab-pane>
217+
210218
</el-tabs>
211219
</div>
212220

@@ -243,6 +251,7 @@
243251
import ServerController from '../ApiClient/ServerController';
244252
import MessageViewPlainText from "./messageviewplaintext.vue";
245253
import MessageCompose from "@/components/messagecompose.vue";
254+
import MessagePartPreview from "@/components/messagepartpreview.vue";
246255
import { UseDark } from '@vueuse/components';
247256
248257
@Component({
@@ -256,6 +265,7 @@
256265
messageclientanalysis: MessageClientAnalysis,
257266
messagehtmlvalidation: MessageHtmlValidation,
258267
messagecompose: MessageCompose,
268+
messagepartpreview: MessagePartPreview,
259269
UseDark
260270
}
261271
})
@@ -444,6 +454,13 @@
444454
get selectedPartHeaders() {
445455
return this.selectedPart != null ? this.selectedPart.headers : [];
446456
}
457+
458+
get isSelectedPartPreviewable(): boolean {
459+
if (!this.selectedPart) return false;
460+
const header = this.selectedPart.headers.find(h => h.name.toLowerCase() === 'content-type');
461+
const contentType = header ? header.value.split(';')[0].trim().toLowerCase() : '';
462+
return contentType.startsWith('image/') || contentType === 'text/html' || contentType === 'application/pdf';
463+
}
447464
}
448465
449466
export default toNative(MessageView);

Rnwood.Smtp4dev/Controllers/MessagesController.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,9 @@ public async Task<IActionResult> RelayMessage(Guid id, [FromBody] MessageRelayOp
288288
[SwaggerResponse(System.Net.HttpStatusCode.OK, typeof(string), Description = "")]
289289
[SwaggerResponse(System.Net.HttpStatusCode.NotFound, typeof(void), Description = "If the message or part does not exist")]
290290
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = CACHE_DURATION)]
291-
public async Task<FileStreamResult> GetPartContent(Guid id, string partid)
291+
public async Task<FileStreamResult> GetPartContent(Guid id, string partid, bool download=false)
292292
{
293-
return ApiModel.Message.GetPartContent(await GetMessage(id), partid);
293+
return ApiModel.Message.GetPartContent(await GetMessage(id), partid, download);
294294
}
295295

296296
/// <summary>

0 commit comments

Comments
 (0)