mirror of
https://github.com/ApfelTeeSaft/R2-Explorer.git
synced 2026-08-26 19:33:39 +00:00
Fix some email issues and start working on sending emails via mailchannels
This commit is contained in:
@@ -62,8 +62,11 @@ wrangler publish
|
||||
- allow bucket names with spaces
|
||||
- Search files
|
||||
- Rename folders
|
||||
- Delete folders
|
||||
- Image thumbnail's using Cloudflare workers
|
||||
- Image thumbnail's ?
|
||||
- Object detection on images using workers-ai ?
|
||||
- Tooltip when hovering a file with absolute time in "x days time ago" format
|
||||
- bundle bootstrap icons instead of importing
|
||||
- support for responding to emails
|
||||
|
||||
## Known issues
|
||||
|
||||
- Email inline images and assets don't load when using basic auth
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"dev": "quasar dev",
|
||||
"build": "quasar build",
|
||||
"deploy": "wrangler pages deploy --branch main --project-name r2-explorer-dashboard dist/",
|
||||
"deploy-dev": "wrangler pages deploy --branch dev --project-name r2-explorer-dashboard dist/"
|
||||
"deploy": "wrangler pages deploy --branch main --project-name r2-explorer-dashboard dist/spa/",
|
||||
"deploy-dev": "wrangler pages deploy --branch dev --project-name r2-explorer-dashboard dist/spa/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.16.9",
|
||||
|
||||
@@ -44,9 +44,10 @@
|
||||
|
||||
<q-card-actions vertical>
|
||||
<div class="overflow-auto d-block email-wrapper">
|
||||
<iframe v-if="file.html" frameborder="0" scrolling="no" class="w-100 d-block" @load="contentFinishedLoading"
|
||||
<iframe v-if="srcdoc" frameborder="0" scrolling="no" class="w-100 d-block" @load="contentFinishedLoading"
|
||||
ref="renderWindow"
|
||||
:srcdoc="file.html"
|
||||
id="renderWindow"
|
||||
:srcdoc="srcdoc"
|
||||
sandbox="allow-popups allow-popups-to-escape-sandbox allow-same-origin"
|
||||
csp="script-src 'none'"
|
||||
/>
|
||||
@@ -55,22 +56,22 @@
|
||||
</q-card-actions>
|
||||
|
||||
|
||||
<q-card-actions vertical v-if="file.attachments.length > 0">
|
||||
<q-card-actions vertical v-if="attachments.length > 0">
|
||||
<q-separator/>
|
||||
<h6 class="q-my-md">Attachments</h6>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4" v-for="attachment of file.attachments" :key="attachment.filename">
|
||||
<div class="row attachments">
|
||||
<div v-for="attachment of file.attachments" class="col-md-4 col-sm-12" :key="attachment.filename">
|
||||
|
||||
<q-card>
|
||||
<q-card-section class="q-pa-sm flex" style="align-items: center">
|
||||
<q-icon name="description" size="md" color="blue" class="q-mr-sm"/>
|
||||
{{ attachment.filename }}
|
||||
<q-btn color="white" text-color="black" icon="download" class="q-mr-0 q-ml-auto" @click="downloadAtt(attachment)" />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-card>
|
||||
<q-card-section class="q-pa-sm flex" style="align-items: center">
|
||||
<q-icon name="description" size="md" color="blue" class="q-mr-sm"/>
|
||||
{{ attachment.filename }}
|
||||
<q-btn color="white" text-color="black" icon="download" class="q-mr-0 q-ml-auto" @click="downloadAtt(attachment)" />
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
@@ -100,9 +101,11 @@ export default defineComponent({
|
||||
name: "EmailFolderPage",
|
||||
data: function() {
|
||||
return {
|
||||
srcdoc: null,
|
||||
file: null,
|
||||
fileHead: null,
|
||||
timeInterval: null
|
||||
timeInterval: null,
|
||||
attachments: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -146,10 +149,37 @@ export default defineComponent({
|
||||
const fileData = await apiHandler.downloadFile(this.selectedBucket, this.filePath, {})
|
||||
|
||||
const filename = fileName.split(".json")[0];
|
||||
for (const att of fileData.data.attachments) {
|
||||
att.downloadUrl = `${this.mainStore.serverUrl}/api/buckets/${this.selectedBucket}/${encode(`.r2-explorer/emails/${this.selectedFolder}/${filename}/${att.filename}`)}`;
|
||||
}
|
||||
|
||||
this.file = fileData.data;
|
||||
let htmlContent = fileData.data.html
|
||||
|
||||
if (htmlContent) {
|
||||
// Add target blank to all links
|
||||
htmlContent = htmlContent.replaceAll(/<a(.*?)>(.*?)<\/a>/gi, '<a$1 target="_blank">$2</a>');
|
||||
|
||||
// Inject attachment url and replace in html to point to correct path
|
||||
for (const att of fileData.data.attachments) {
|
||||
att.display = true
|
||||
att.downloadUrl = `${this.mainStore.serverUrl}/api/buckets/${this.selectedBucket}/${encode(`.r2-explorer/emails/${this.selectedFolder}/${filename}/${att.filename}`)}`;
|
||||
|
||||
let contentId = att.contentId
|
||||
if (contentId){
|
||||
if (contentId.startsWith('<') && contentId.endsWith('>')){
|
||||
contentId = contentId.substring(1, contentId.length-1);
|
||||
}
|
||||
|
||||
const matchString = `cid:${contentId}`
|
||||
if (htmlContent.includes(matchString)) {
|
||||
htmlContent = htmlContent.replaceAll(`cid:${contentId}`, att.downloadUrl)
|
||||
att.display = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.srcdoc = htmlContent
|
||||
}
|
||||
|
||||
this.attachments = fileData.data.attachments.filter((obj) => obj.display)
|
||||
|
||||
apiHandler.headFile(this.selectedBucket, this.filePath).then(async (obj) => {
|
||||
if (obj.customMetadata.read === 'false') {
|
||||
@@ -162,6 +192,10 @@ export default defineComponent({
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout(function() {
|
||||
self.contentFinishedLoading()
|
||||
}, 10000)
|
||||
|
||||
this.timeInterval = setInterval(function() {
|
||||
self.resizeIframe()
|
||||
}, 400);
|
||||
@@ -223,4 +257,12 @@ export default defineComponent({
|
||||
iframe {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
gap: 10px;
|
||||
|
||||
@media (max-width: 992px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -279,7 +279,7 @@ export default defineComponent({
|
||||
const files = response.data.objects.filter(function(obj) {
|
||||
return !obj.key.endsWith("/"); // Remove selected folder
|
||||
}).map(function(obj) {
|
||||
const date = new Date(obj.uploaded);
|
||||
const date = new Date(parseInt(obj.customMetadata.timestamp));
|
||||
|
||||
return {
|
||||
...obj,
|
||||
@@ -288,7 +288,7 @@ export default defineComponent({
|
||||
has_attachments: obj.customMetadata.has_attachments === "true",
|
||||
read: obj.customMetadata.read,
|
||||
lastModified: timeSince(date),
|
||||
timestamp: date.getTime()
|
||||
timestamp: parseInt(obj.customMetadata.timestamp)
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const baseConfig = {
|
||||
readonly: false,
|
||||
cors: true,
|
||||
showHiddenFiles: true,
|
||||
// dashboardUrl: "https://dashboard-v1.r2-explorer-dashboard.pages.dev/",
|
||||
dashboardUrl: "https://dev.r2-explorer-dashboard.pages.dev/",
|
||||
cacheAssets: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
|
||||
import { Context } from "../../interfaces";
|
||||
import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
|
||||
export class SendEmail extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: "post-email-send",
|
||||
tags: ["Emails"],
|
||||
summary: "Send Email",
|
||||
requestBody: {
|
||||
subject: "Look! No servers",
|
||||
from: {
|
||||
email: "[email protected]",
|
||||
name: "Workers - MailChannels integration"
|
||||
},
|
||||
to: [{ email: "[email protected]", name: "Test Recipient" }],
|
||||
content: {
|
||||
"text/plain": "And no email service accounts and all for free too!"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({ msg: "unauthorized" }, { status: 401 });
|
||||
|
||||
const emailResp = await fetch("https://api.mailchannels.net/tx/v1/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
personalizations: [
|
||||
{
|
||||
to: data.body.to
|
||||
}
|
||||
],
|
||||
from: data.body.from,
|
||||
subject: data.body.subject,
|
||||
content: [
|
||||
{
|
||||
type: "text/plain",
|
||||
value: data.body.content["text/plain"]
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
return { msg: await emailResp.text() };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { config } from "../settings";
|
||||
import { OpenAPIRouter } from "@cloudflare/itty-router-openapi";
|
||||
import { SendEmail } from "./api/sendEmail";
|
||||
|
||||
|
||||
export const emailsRouter = OpenAPIRouter({
|
||||
base: "/api/emails",
|
||||
raiseUnknownParameters: config.raiseUnknownParameters,
|
||||
generateOperationIds: config.generateOperationIds
|
||||
});
|
||||
|
||||
emailsRouter.post("/send", SendEmail);
|
||||
@@ -8,6 +8,7 @@ import { receiveEmail } from "./emails/receiveEmail";
|
||||
import { serverRouter } from "./server/router";
|
||||
import { config as settings } from "./settings";
|
||||
import { validateBasicAuth } from "./authentication/api/basic";
|
||||
import { emailsRouter } from "./emails/router";
|
||||
|
||||
export function R2Explorer(config?: R2ExplorerConfig) {
|
||||
config = config || {};
|
||||
@@ -59,6 +60,7 @@ export function R2Explorer(config?: R2ExplorerConfig) {
|
||||
|
||||
router.all("/api/server/*", serverRouter);
|
||||
router.all("/api/buckets/*", bucketsRouter);
|
||||
router.all("/api/emails/*", emailsRouter);
|
||||
|
||||
router.original.get("*", dashboardProxy);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user