Graph API: Send mail with attachment by drive item ID? - php

I'd like to send mail via the Graph API and attach a file by its drive item ID.
At the moment I can successfully send email via the Graph API and attach a file that is on my local server.
However the file originates on OneDrive so the current situation is I have to download the file to my server then re-upload it via the sendMail endpoint as an attachment and then delete it from my server.
This seems like an unneeded step if it's possible to just provide the file ID and let office 365 resolve it all locally.
$mailBody = ...
'attachments' => [
[
'#odata.type' => '#microsoft.graph.fileAttachment',
'Name' => 'file.docx',
'ContentBytes' => $localFile
// 'DriveID' => 'possibly this instead of Content Bytes?'
]
]
...
$response = $this->getGraph()->createRequest("POST", "/users/{primary-user}/sendMail")
->attachBody($mailBody)
->execute();

You can attach a file by its drive item ID, it is called referenceAttachment but not the fileAttachment in your code. The v1.0 edition has very limited support for referenceAttachment. And by that I mean, there isn't much you can do with them beyond acknowledging one exists.
Reference from egorbunov's answer: Send reference attachment to email via Graph API
Create the message draft using POST request to https://graph.microsoft.com/beta/me/messages with payload:
{
"subject": "TestMessage",
"toRecipients": [
{
"emailAddress":{
"address":"egor-mailbox#ya.ru"
}
}
],
"body": {
"contentType": "html",
"content": "<b>Hello!</b>"
}
},
As a response you will get the whole message structure with id set to something like
AQMkADAwATMwMAItMTJkYi03YjFjLTAwAi0wMAoARgAAA_hRKmxc6QpJks9QJkO5R50HAP6mz4np5UJHkvaxWZjGproAAAIBDwAAAP6mz4np5UJHkvaxWZjGproAAAAUZT2jAAAA.
Lets refer to it as {messageID}. NOTE: as you can see I have passed
html-typed body. This is needed because (at least in GraphAPI
Explorer) graph api returns error in case you are trying to add
reference attachment to message with non-html body content-type.
After that you can create an attachment using POST request to https://graph.microsoft.com/beta/me/messages/{messageID}/attachments
{
"#odata.type": "#microsoft.graph.referenceAttachment",
"name": "AttachmentName",
"sourceUrl": "https://1drv.ms/u/s!ASDLKASDLASHDLASKDLJAXCXZ_DASD",
"providerType": "oneDriveConsumer",
"isFolder": false
}
After step 2 you will see created message in your mailbox Drafts folder. To send it use
https://graph.microsoft.com/beta/me/messages/{messageID}/send (=(
turns out it does not work too)
An alternative solution, not add attachment but add the file link in the mail body directly(Let the recipient to download it as needed).

Related

How can I send a Firebase Cloud Messaging notification to all devices in laravel [duplicate]

I'm attempting to send out a notification to all app users (on Android), essentially duplicating what happens when a notification is sent via the Firebase admin console. Here is the CURL command I begin with:
curl --insecure --header "Authorization: key=AIzaSyBidmyauthkeyisfineL-6NcJxj-1JUvEM" --header "Content-Type:application/json" -d "{\"notification\":{\"title\":\"note-Title\",\"body\":\"note-Body\"}}" https://fcm.googleapis.com/fcm/send
Here's that JSON parsed out to be easier on your eyes:
{
"notification":{
"title":"note-Title",
"body":"note-Body"
}
}
The response that comes back is just two characters:
to
That's it, the word "to". (Headers report a 400) I suspect this has to do with not having a "to" in my JSON. What would one even put for a "to"? I have no topics defined, and the devices have not registered themselves for anything. Yet, they are still able to receive notifications from the Firebase admin panel.
I'm want to attempt a "data only" JSON package due to the amazing limitation of Firebase notification processing whereby if your app is in the foreground, the notification gets processed by YOUR handler, but if your app is in the background, it gets processed INTERNALLY by the Firebase service and never passed to your notification handler. APPARENTLY this can be worked around if you submit your notification request via the API, but ONLY if you do it with data-only. (Which then breaks the ability to handle iOS and Android with the same message.) Replacing "notification" with "data" in any of my JSON has no effect.
Ok, then I attempted the solution here: Firebase Java Server to send push notification to all devices
which seems to me to say "Ok, even though notifications to everyone is possible via the Admin console... it's not really possible via the API." The workaround is to have each client subscribe to a topic, and then push out the notification to that topic. So first the code in onCreate:
FirebaseMessaging.getInstance().subscribeToTopic("allDevices");
then the new JSON I send:
{
"notification":{
"title":"note-Title",
"body":"note-Body"
},
"to":"allDevices"
}
So now I'm getting a real response from the server at least. JSON response:
{
"multicast_id":463numbersnumbers42000,
"success":0,
"failure":1,
"canonical_ids":0,
"results":
[
{
"error":"InvalidRegistration"
}
]
}
And that comes with a HTTP code 200. Ok... according to https://firebase.google.com/docs/cloud-messaging/http-server-ref a 200 code with "InvalidRegistration" means a problem with the registration token. Maybe? Because that part of the documentation is for the messaging server. Is the notification server the same? Unclear. I see elsewhere that the topic might take hours before it's active. It seems like that would make it useless for creating new chat rooms, so that seems off as well.
I was pretty excited when I could code up an app from scratch that got notifications in just a few hours when I had never used Firebase before. It seems like it has a long way to go before it reaches the level of, say, the Stripe.com documentation.
Bottom line: does anyone know what JSON to supply to send a message out to all devices running the app to mirror the Admin console functionality?
Firebase Notifications doesn't have an API to send messages. Luckily it is built on top of Firebase Cloud Messaging, which has precisely such an API.
With Firebase Notifications and Cloud Messaging, you can send so-called downstream messages to devices in three ways:
to specific devices, if you know their device IDs
to groups of devices, if you know the registration IDs of the groups
to topics, which are just keys that devices can subscribe to
You'll note that there is no way to send to all devices explicitly. You can build such functionality with each of these though, for example: by subscribing the app to a topic when it starts (e.g. /topics/all) or by keeping a list of all device IDs, and then sending the message to all of those.
For sending to a topic you have a syntax error in your command. Topics are identified by starting with /topics/. Since you don't have that in your code, the server interprets allDevices as a device id. Since it is an invalid format for a device registration token, it raises an error.
From the documentation on sending messages to topics:
https://fcm.googleapis.com/fcm/send
Content-Type:application/json
Authorization:key=AIzaSyZ-1u...0GBYzPu7Udno5aA
{
"to": "/topics/foo-bar",
"data": {
"message": "This is a Firebase Cloud Messaging Topic Message!",
}
}
The most easiest way I came up with to send the push notification to all the devices is to subscribe them to a topic "all" and then send notification to this topic.
Copy this in your main activity
FirebaseMessaging.getInstance().subscribeToTopic("all");
Now send the request as
{
"to": "/topics/all",
"data":
{
"title":"Your title",
"message":"Your message"
"image-url":"your_image_url"
}
}
This might be inefficient or non-standard way, but as I mentioned above it's the easiest. Please do post if you have any better way to send a push notification to all the devices.
Just checked the FCM documentation, this is the only way to send notifications to all the devices (as of 8th July 2022).
As mentioned in the comments, the notification is not automatically displayed, you have to define a class that is derived from FirebaseMessagingService and then override the function onMessageReceived.
First register your service in app manifest.
<!-- AndroidManifest.xml -->
<service
android:name=".java.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Add these lines inside the application tag to set the custom default icon and custom color:
<!-- AndroidManifest.xml -->
<!-- Set custom default icon. This is used when no icon is set
for incoming notification messages. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="#drawable/ic_stat_ic_notification" />
<!-- Set color used with incoming notification messages. This is used
when no color is set for the incoming notification message. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="#color/colorAccent" />
Now create your service to receive the push notifications.
// MyFirebaseMessagingService.java
package com.google.firebase.example.messaging;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "MyFirebaseMsgService";
// [START receive_message]
#Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.d(TAG, "From: " + remoteMessage.getFrom());
// Check if message contains a data payload.
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Message data payload: " + remoteMessage.getData());
if (/* Check if data needs to be processed by long running job */ true) {
// For long-running tasks (10 seconds or more) use WorkManager.
scheduleJob();
} else {
// Handle message within 10 seconds
handleNow();
}
}
// Check if message contains a notification payload.
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
}
}
// [END receive_message]
// [START on_new_token]
/**
* There are two scenarios when onNewToken is called:
* 1) When a new token is generated on initial app startup
* 2) Whenever an existing token is changed
* Under #2, there are three scenarios when the existing token is changed:
* A) App is restored to a new device
* B) User uninstalls/reinstalls the app
* C) User clears app data
*/
#Override
public void onNewToken(#NonNull String token) {
Log.d(TAG, "Refreshed token: " + token);
// If you want to send messages to this application instance or
// manage this apps subscriptions on the server side, send the
// FCM registration token to your app server.
sendRegistrationToServer(token);
}
// [END on_new_token]
private void scheduleJob() {
// [START dispatch_job]
OneTimeWorkRequest work = new OneTimeWorkRequest.Builder(MyWorker.class)
.build();
WorkManager.getInstance(this).beginWith(work).enqueue();
// [END dispatch_job]
}
private void handleNow() {
Log.d(TAG, "Short lived task is done.");
}
private void sendRegistrationToServer(String token) {
// TODO: Implement this method to send token to your app server.
}
public static class MyWorker extends Worker {
public MyWorker(#NonNull Context context, #NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
#NonNull
#Override
public Result doWork() {
// TODO(developer): add long running task here.
return Result.success();
}
}
}
You can follow this tutorial if you're new to sending push notifications using Firebase Cloud Messaging Tutorial - Push Notifications using FCM and Send messages to multiple devices - Firebase Documentation
To send a message to a combination of topics, specify a condition, which is a boolean expression that specifies the target topics. For example, the following condition will send messages to devices that are subscribed to TopicA and either TopicB or TopicC:
{
"data":
{
"title": "Your title",
"message": "Your message"
"image-url": "your_image_url"
},
"condition": "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"
}
Read more about conditions and topics here on FCM documentation
EDIT: It appears that this method is not supported anymore (thx to #FernandoZamperin). Please take a look at the other answers!
Instead of subscribing to a topic you could instead make use of the condition key and send messages to instances, that are not in a group. Your data might look something like this:
{
"data": {
"foo": "bar"
},
"condition": "!('anytopicyoudontwanttouse' in topics)"
}
See https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_topics_2
One way to do that is to make all your users' devices subscribe to a topic. That way when you target a message to a specific topic, all devices will get it. I think this how the Notifications section in the Firebase console does it.
I was looking solution for my Ionic Cordova app push notification.
Thanks to Syed Rafay's answer.
in app.component.ts
const options: PushOptions = {
android: {
topics: ['all']
},
in Server file
"to" => "/topics/all",
Check your topic list on firebase console.
Go to firebase console
Click Grow from side menu
Click Cloud Messaging
Click Send your first message
In the notification section, type something for Notification title and Notification text
Click Next
In target section click Topic
Click on Message topic textbox, then you can see your topics (I didn't created topic called android or ios, but I can see those two topics.
When you send push notification add this as your condition.
"condition"=> "'all' in topics || 'android' in topics || 'ios' in topics",
Full body
array(
"notification"=>array(
"title"=>"Test",
"body"=>"Test Body",
),
"condition"=> "'all' in topics || 'android' in topics || 'ios' in topics",
);
If you have more topics you can add those with || (or) condition, Then all users will get your notification. Tested and worked for me.
Just make all users who log in subscribe to a specific topic, and then send a notification to that topic.
For anyone wondering how to do it in cordova hybrid app:
go to index.js -> inside the function onDeviceReady() write :
subscribe();
(It's important to write it at the top of the function!)
then, in the same file (index.js) find :
function subscribe(){
FirebasePlugin.subscribe("write_here_your_topic", function(){
},function(error){
logError("Failed to subscribe to topic", error);
});
}
and write your own topic here -> "write_here_your_topic"
It is a PHP Admin-SDK example to subscribe an user to a Topic and to send messages to a device by device token or to a Topic. Note that the Topic is created automatically when you subscribe an user.
$testTokens = ['device token 1', 'device token 2', ....]
// CREDENTIALS, YOU HAVE TO DOWNLOAD IT FROM FIREBASE CONSOLE.
$factory = (new Factory())->withServiceAccount('credentials.json');
$messaging = $factory->createMessaging();
// Subscribe a token or a group of tokens to a topic (this topic is created automatically if not exists)
// YOU CAN DO THIS IN THE MOBILE APP BUT IS BETTER DO IT IN THE API.
$result = $messaging->subscribeToTopic('all', $testTokens); // 'all' is the topic name
// Send a message to a specific Topic (Channel)
$message = CloudMessage::withTarget('topic', 'all')
->withNotification(Notification::create('Global message Title', 'Global message Body'))
->withData(['key' => 'value']); // optional
$messaging->send($message);
// Send a message to a token or a grup of tokens (ONLY!!!)
foreach($testTokens as $i=>$token){
$message = CloudMessage::withTarget('token', $token)
->withNotification(Notification::create('This is the message Title', 'This is the message Body'))
->withData(['custom_index' => $i]); // optional
$messaging->send($message);
You can check this repo for more details: firebase-php-admin-sdk
Your can send notification to all devices using "/topics/all"
https://fcm.googleapis.com/fcm/send
Content-Type:application/json
Authorization:key=AIzaSyZ-1u...0GBYzPu7Udno5aA
{
"to": "/topics/all",
"notification":{ "title":"Notification title", "body":"Notification body", "sound":"default", "click_action":"FCM_PLUGIN_ACTIVITY", "icon":"fcm_push_icon" },
"data": {
"message": "This is a Firebase Cloud Messaging Topic Message!",
}
}

Slack Incoming Webhooks - Change Action URL?

I'm unclear after reading through Slack's documentation around Incoming Webhooks. It seems like there's some ambiguity around Incoming Webhooks vs. posts through their chat API. It sounds like if I call it from https://my-application.yxz/api/notify?key=123123123123, and post to Slack from there, then Slack's action buttons will reply to that same URL, but I can't confirm that.
https://api.slack.com/docs/message-attachments
I'm successfully sending messages to the specified channel, but I can't figure out how to get a response back, or how to change the URL the actions will send their own payload to. Is that even possible?
For context of the code below, I'm using Laravel. Ideally, I wanted to only retain a Slack Webhook URL in the user settings for their store's location so they could receive Slack notifications triggered by our API being hit.
This is the payload I'm sending to the webhook and it works fine, however I would like to add actionable buttons and post the payload back to my external site. I'm not sure if Incoming Webhooks are even the right approach. Can someone shed some light on this?
# $helpdesk => contains webhook URL and other details
# $prompt => just some text
# $prompt_id => id associated with prompt object passed into the function this is found in
# $device => the source of this prompt
$payload = json_encode([
"text" => "A customer at $device->name requires attention",
"username"=>"Our Alert Thing",
"icon_emoji"=>":wave:",
"attachments"=> [
[
"fallback"=> $prompt,
"author_name"=> "Customer # $device->name",
"title"=> $prompt,
"text"=> "Let them know if you can help",
"callback_id"=> "csprompt_".$prompt_id,
"color"=> "#24ACE2",
"actions"=> [
[
"name"=> "action",
"type"=> "button",
"text"=> "I call dibs",
"style"=> "primary",
"value"=> "helping",
],
]
]
]
]);
$res = $client->post($helpdesk->slack_webhook, ['body'=>$payload]);
You can not programmatically set your action URL. Slack will send all interactive message requests (e.g. from a user clicking on one of your buttons) to the "request URL" that you have configured for your Slack app.
Check this link for details on where to find the request URL.
I think technically you can use incoming webhooks, but I would recommend using the API with chat.postMessage, because incoming webhooks are very limited. e.g. they can only be used with one pre-configured channel.

How to create a Slack message containing an uploaded image?

I'd like to create a message in the #general channel of my Slackspace from within a PHP script. The message should contain text and an image which was created locally on-the-fly.
I've already created an App, generated an bearer token and have managed to create an text-only message also as an image-upload.
But i didn't know how to create both in one message, as the procedure above creates two messages, one with text and another one with the image.
There are two different approaches on how to post a Slack message with an image.
A. Upload image directly with message
You can upload an image directly to Slack and share it in a channel. Within that request you can also add a comment that will appear as message above the images. This is the easiest approach, however you comment is limited to one string.
API method: files.upload with these arguments:
channels: ID of one or multiple channel for the image to appear in
initial_comment: Your message
B. Post message with image block / attachment
Alternatively you can add an image to your normal message either as image block or secondary attachment. This only works with a public URL to your image file, so you first need to upload your image to an image hoster (which can be your Slack workspace) to get the public URL.
In our example we will use Slack as image hoster, but you can use any image hoster (e.g. Imgur) even your own webserver, as long as you get a public URL for your image file.
Step 1 - Upload image to Slack
API method: files.upload with no special arguments, but make sure to get the file ID from the response. Don't include the channels argument or the image will be posted visible into those channel.
Step 2 - Create public URL
Next you have to mark the uploaded file as public. Only then will it be accessible through its public_url property
API method: files.sharedPublicURL with the file ID as argument.
Next you need to construct the direct image link from the link to website / permalink_public property of the file.
The website link you get from permalink_public has the format:
https://slack-files.com/{team_id}-{file_id}-{pub_secret}
The direct link to the image has the format:
https://files.slack.com/files-pri/{team_id}-{file_id}/{filename}?pub_secret={pub_secret}
So you just need to extract the pub_secret from permalink_public and you should be able to construct the direct link to the image. The other parameters you can get from your file object.
Step 3 - Send message
Finally compose your message with the image URL either as Image Block or in a secondary attachment and submit it using a method of your choice.
API method: chat.PostMessage or any other method for sending message incl. incoming webhooks.
Answer to OP
If you need to stick with webhooks as it appears from your comments and have no access to the Slack API I would suggest uploading the image to an image hoster (e.g. Imgur) and then use approach B.
See also
Slack bot send an image
Can I upload an image as attachment with Slack API?
How to use the permalink_public URL of an uploaded image to include it in a message?
After much tinkering I found that while I could not use the API to create a message and upload an image simultaneously, I can first upload an image and then, with the timestamp returned, use update message to add text to the original message with the image upload.
This is the flow:
1- Use files_upload method to upload an image to my channel (using the channel name)
response = client.files_upload(
channels=my_channel_name,
file=image_path,
initial_comment='My initial comment'
)
2- Get the response from the files_upload and extract the channel id and timestamp of the message.
channel_id = response['file']['groups'][0]
ts = response['file']['shares']['private'][channel_id][0]['ts']
3- Use chat update to add text or rich content to the message with the uploaded image:
response = client.chat_update(
channel=channel_id,
text="My Message",
ts=ts,
blocks=blocks_list
)
For those who might still need this.. this gist helped me. Its a quick and easier way using GuzzleHttp.
use GuzzleHttp\Client;
/**
* Notes:
*
* Tested with guzzlehttp/guzzle version 7.3
* See https://api.slack.com/methods/files.upload for details on how to generate a token
*
*/
$fileName = '';
$filePath = '';
$slacktoken = ''; // See https://api.slack.com/tokens; The token will need file.read and file.write permissions
$client = new Client();
$apiUrl = 'https://slack.com/api/files.upload';
$client = new Client();
$request = $client->post( $apiUrl, [
'headers' => ['Authorization' => 'auth_trusted_header'],
'multipart' => [
[
'name' => 'token',
'contents' => $slacktoken,
],
[
'name' => 'file',
'contents' => fopen($filePath, 'r'),
'filename' => $fileName
],
[
'name' => 'channels',
'contents' => '#general'
],
[
'name' => 'initial_comment',
'contents' => 'File Uploaded'
]
]
]);
var_dump($request->getBody()->getContents());

Detect Disposable Email with Garbage Domain

I am developing website using php/codeigniter.
I have downloaded a list of temporary email domains from github (https://gist.github.com/adamloving/4401361)
I integrated this to my website to filter and validate email address.But I noticed that some domains are garbage and cannot detect by the list provided.
Please image below.
Currently Im using this code to filter/validate emails:
public function is_temp_mail($mail='')
{
$this->db->select('domain');
$this->db->from('table_disposal_email_domains');
$domains=$this->db->get()->result();
foreach($domains as $domain)
{
list(,$mail_domain) = explode('#',$mail);
if(strcasecmp($mail_domain, $domain->domain) == 0){
return true;
}
}
return false;
}
How to block garbage domains.Please help.
One of the issue with disposable emails is that new domains are added daily. So, maintaining your own list isn't gonna be enough after a few days.
You can use the validator.pizza API, which is free and updated frequently.
Disclaimer: I made this API 😊
I wrote a simple API for determining the domains of temporary mails, all you need to determine the temporary mail is to send a GET request:
https://api.testmail.top/domain/check/data=example#mail.com&ip=8.8.8.8
with authorization header:
Authorization: Bearer XXXXXXXXXX.XXXXXXXXXX.XXXXXXXXXX
and in response you will receive a message like this if the mail turns out to be temporary:
{
"error": 0,
"result": false,
"message": "This domain is in Blacklist"
}
you will receive such an answer if the mail turns out to be trusted (something like gmail.com or yahoo.com):
{
"error": 0,
"result": true,
"message": "This domain is in Whitelist"
}
I have described error codes and more detailed instructions on this page
It would be good if you use a third party package to help you on blocking temporary email domains. You can use MailboxValidator API, which had 300 free API credits per month. You can use the free API key with MailboxValidator CodeIgniter Email Validation Package after sign up.
Disclaimer: I am working at MailboxValidator.

Sending a message by the WhatsApp API using PHP

I am trying to send a message by the WhatsApp API using PHP. I have the WhatsApp password and am getting by WART using the following code:
<?php
require "whatsapp.class.php";
// DEMO OF USAGE
$wa = new WhatsApp("91XXXXXXXXXX", "XXX-XXX", "Nick Name");
$wa->Connect();
$t = $wa->Login();
$wa->Message("5","91XXXXXXXXXX","Good code");
echo "Message sent";
?>
I did not change anything in the whatsapp.class.php file.
My files are:
http://vvsindia.com/stackoverflow/whatsapp.class.txt
http://vvsindia.com/stackoverflow/func.txt
http://vvsindia.com/stackoverflow/decode.txt
For your convenience to view while browsing, I just uploaded them as a txt file, but originally these are PHP files.
Using the above code I was not able to send any message. What could the issue be?
You can use the below script to send a message from WhatsApp in PHP.
https://github.com/venomous0x/WhatsAPI/tree/master/examples
Configure the source code in Apache and run examples/whatsapp.php file.
You have to change the below configurations.
// Simple password to view this script
$config['webpassword'] = 'MakeUpPassword';
and
$config['YOURNAME'] = array(
'id' => 'e807f1fcf82d132f9bb018ca6738a19f',
'fromNumber' => '441234567890',
'nick' => "YOURNICKNAME",
'waPassword' => "EsdfsawS+/ffdskjsdhwebdgxbs=",
'email' => 'testemail#gmail.com',
'emailPassword' => 'gmailpassword'
);
You should rather try this quick and easy interface API:
https://www.mashape.com/motp/whatsapp-pusher
As given in the documentation, sending a text message to a WhatsApp user would be a one-step process. Below is a sample cURL call to send a text message to a WhatsApp user.
curl -XPOST 'http://api.dial2verify.com/WHAPP/SEND/<API_KEY>/<Phone_ISD>' \
-d 'Msg=Text to image URL here'
To get the API key, you are required to drop a request to hello#dial2verify.in, and they would provide you a free API key.
Phone_ISD: should be a complete phone number including ISD code (for example, 919922003300).

Categories